Files
Deal/src/core/Deal.Modules.Kanban/Application/Services/CardsService.cs
T
Rustam Khalimov 492950bdd0 Отформатировать списки параметров по код-стайлу
Больше двух параметров — каждый на отдельной строке (закрывающая
скобка в конце последнего); два и меньше — в одну строку. Правило
добавлено в docs/spec/Код-стайл-Дейл.md; применено к 628 сигнатурам
в 253 файлах.
2026-09-11 13:22:56 +03:00

129 lines
8.6 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.Contracts.Integrations;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.ColumnRules;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
using Deal.Modules.Settings.Application.Registrars;
using Deal.Modules.Settings.Application.Services;
// Алиас: статический класс ColumnRules лежит в одноимённом пространстве имён — внутри пространства имён
// Deal.Modules.Kanban.Application имя ColumnRules резолвится в пространство (CS0234), нужен явный алиас.
using KanbanColumnRules = Deal.Modules.Kanban.Application.ColumnRules.ColumnRules;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
using Deal.Modules.Kanban.Application.Registrars;
namespace Deal.Modules.Kanban.Application.Services;
/// <summary>
/// Сервис карточек — единый домен карточки (дашборд и «Выбранные»): leads.py L151279 + L509551
/// и projects.py L103282 (этап 9).
/// </summary>
/// <remarks>
/// Чистый сервис модуля (без EF/HTTP): оркестрирует <see cref="ICardStore"/> (карточки/контейнеры/
/// комментарии/журнал), <see cref="ISettingsStore"/> (кэш курсов <c>ratesCache</c>, настройка напоминаний,
/// Ruling 3/7), <see cref="IMlClient"/> (обучающие сигналы move/trash/restore и счётчики counts) и
/// <see cref="IFileStorage"/> (объекты вложений карточки).
/// Полные карточки (JSON-поля, comments, human-метка time, matchHits) собирает адаптер (маппинг
/// строки → CardDto в хранилище); сервис добавляет поведение: валидации с фиксированными текстами прототипа,
/// пересчёт matchHits при размещении в доску (Ruling 2, ColumnRules), журнал CardMoves и push-сигналы ML,
/// операции пространства «Выбранные» (создание, патч, ссылки, файлы, перенос по стадии с историей,
/// напоминания) на той же сущности карточки.
/// Обучение ML идёт ВСЕГДА и синхронно — выключатель <c>mlEnabled</c> управляет только использованием ML в
/// пайплайне, а не записью действий пользователя (ml_client.py L6–7: «обучение идёт всегда»; Ruling 4).
/// 404-семантика «Карточка не найдена» выражается null-результатом методов; тексты 400 — константы класса.
/// C32: класс разделён на partial-файлы по темам (Operations/Helpers/Selected/Files/Reminders).
/// </remarks>
/// <param name="store">Единый порт хранилища карточек/контейнеров/журнала тенанта.</param>
/// <param name="settings">KV-хранилище настроек тенанта (курсы, напоминания).</param>
/// <param name="mlClient">Клиент ML: PushAsync — обучающий сигнал действия, StatusAsync — счётчики counts.</param>
/// <param name="storage">Файловое хранилище вложений карточки (объекты файлов).</param>
public sealed partial class CardsService
{
private readonly ICardStore _store;
private readonly ISettingsStore _settings;
private readonly IMlClient _mlClient;
private readonly IFileStorage _storage;
/// <summary>
/// Создаёт сервис карточек над портами модуля (поле-захват DI-зависимостей).
/// </summary>
/// <param name="store">Единый порт хранилища карточек/контейнеров/журнала тенанта.</param>
/// <param name="settings">KV-хранилище настроек тенанта (курсы, напоминания).</param>
/// <param name="mlClient">Клиент ML: PushAsync — обучающий сигнал действия, StatusAsync — счётчики counts.</param>
/// <param name="storage">Файловое хранилище вложений карточки (объекты файлов).</param>
public CardsService(
ICardStore store,
ISettingsStore settings,
IMlClient mlClient,
IFileStorage storage)
{
_store = store;
_settings = settings;
_mlClient = mlClient;
_storage = storage;
}
// ── Фиксированные строки прототипа (400-детали; leads.py L183184, L239240, dashboard_routes L241) ──
/// <summary>
/// 400 move: целевой контейнер не существует (или это «Неразобранное» — оно допустимо).
/// </summary>
public const string MoveTargetInvalidDetail = "Переносить можно только в существующий контейнер или в «Неразобранное»";
/// <summary>
/// 400 move: исходная колонка archive/trash — из них карточку выводит только restore_lead
/// (кнопка «Вернуть»): прямой перенос в доску миновал бы снятие метки «спам» при возврате из корзины
/// (Ruling 4, PushAsync(spam, 1.0) только в RestoreLeadAsync) и журнал restore.
/// </summary>
public const string MoveSourceRestrictedDetail = "Переносить из корзины, архива или «взятых в работу» нельзя — верните карточку на канбан";
/// <summary>
/// 400 clear-col: колонка не trash/archive (clear_col L239240).
/// </summary>
public const string ClearColInvalidDetail = "Очищать можно только корзину или архив";
/// <summary>
/// 400 комментарий: пустой текст после Trim (dashboard_routes L240241).
/// </summary>
public const string EmptyCommentDetail = "Пустой комментарий";
// ── Журнал CardMoves: действия (leads.py _log_learning L4044) ──────────
// Действие журнала: перенос на доску/в «Неразобранное» (_move L174).
private const string ActionMove = "move";
// Действие журнала: в корзину (_move action='trash' L196).
private const string ActionTrash = "trash";
// Действие журнала: возврат из архива/корзины (restore_lead L216).
private const string ActionRestore = "restore";
// Действие журнала: добавлен комментарий (add_comment L264).
private const string ActionComment = "comment";
/// <summary>
/// Автор комментария — «Вы» (свои комментарии, add_comment L262). Единственный источник строки
/// для комментариев карточки.
/// </summary>
public const string CommentAuthor = "Вы";
/// <summary>
/// Human-метка времени свежего комментария (add_comment L259265; «только что» = возраст < 1 мин).
/// Единственный источник строки: адаптер KanbanStore считает её в HumanAge.
/// </summary>
public const string JustNowLabel = "только что";
// Минимальная длина поискового запроса после Trim: q короче → пустой ответ (search L511512, Ruling 6).
private const int MinSearchQueryLength = 2;
// Ограничение результатов поиска: не больше 12 карточек (search L509, Ruling 6).
private const int SearchLimit = 12;
// Вес сигнала пользователя: действие = истина (ml_client.py USER_WEIGHT L26, Ruling 4).
private const double PushWeightUser = 1.0;
// Вес снятия метки: возврат из корзины (restore_lead L221, delta=-1.0).
private const double PushWeightUnlearn = -1.0;
}