Перейти к основному содержимому

Entity / Aggregate: TicketQueueEntity

Дом полей агрегата «Очередь обращений» контекста support (greenfield). Справочник-конфиг маршрутизации: входящий тикет попадает в очередь по своей категории, а очередь определяет команду поддержки. Реализует матрицу 4.4 «централизованная очередь». Все проекции (модели ответа, списки-фильтры тикетов, payload доменных событий) ссылаются сюда и описывают только дельту.

Назначение

TicketQueueEntity — именованная очередь обращений: контейнер маршрутизации, в который сервис support раскладывает тикеты для последующего разбора командой. Каждая очередь может «владеть» одной категорией тикетов (RoutingCategory) — тогда тикеты этой категории автоматически попадают в неё; ровно одна очередь помечена fallback-ом (IsDefault) для категорий без выделенной очереди (INV-Q-5). Справочник управляется супервизором поддержки (CRUD) и не является пользовательской сущностью — маршрутизация детерминирована и конфигурируется, а не вычисляется. Внешним контекстам очередь не публикуется (внутренний справочник).

Классификация

ПолеЗначение
Контекстsupport
KindAggregateRoot (справочник-конфиг, CRUD без сложной бизнес-логики)
Source of truthsupport
Таблицаsupport.ticket_queues
Tenant isolationTenantId = PIN CompanyId (platform-справочник — очереди централизованные, одни на платформу)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKapp / SEEDИдентификатор очереди.
TenantIdGuidДаindex ix_ticket_queues_tenant_id; часть unique-индексовidentity (platform) / SEEDКомпания ПИН (централизованный справочник).
CodestringДаvarchar(64), не пустое; unique ux_queues_code on (TenantId, Code)app / SEEDСтабильный машинный код (General, Deals, …). Иммутабелен после создания.
NamestringДаvarchar(128), не пустоеapp / SEEDОтображаемое имя очереди (русское).
Descriptionstring?Нетvarchar(1000)appПояснение назначения очереди для супервизора.
DefaultPriorityTicketPriorityДа.HasConversion<string>(), varchar(32), default Normalapp / SEEDПриоритет по умолчанию для тикетов, попавших в очередь (enum — ticket.md).
RoutingCategoryTicketCategory?Нет.HasConversion<string>(), varchar(32); unique partial ux_queues_routing_category on (TenantId, RoutingCategory) where routing_category is not null and status = 'Active'app / SEEDКатегория, тикеты которой авто-маршрутизируются в эту очередь. null — очередь не привязана к категории (напр. default). Enum — ticket.md.
IsDefaultboolДаdefault false; unique partial ux_queues_default on (TenantId) where is_default and status = 'Active'app / SEEDFallback-очередь для нераспознанных категорий. Ровно одна активная (INV-Q-2).
SortOrderintДа>= 0, default 0; index ix_ticket_queues_sort_orderappПорядок отображения очередей в UI/фильтрах.
StatusQueueStatusДа.HasConversion<string>(), varchar(16), index ix_ticket_queues_statusappЖизненный цикл очереди (см. enum и переходы).
CreatedAtDateTimeOffsetДаappСоздание.
CreatedByUserIdGuid?НетappСупервизор (null — SEED-миграция).
UpdatedAtDateTimeOffsetДаappМомент последнего изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?НетappSoft-delete (административно).

Enum QueueStatus

ЗначениеОписание
ActiveРабочая очередь: участвует в маршрутизации, доступна для назначения тикетов.
ArchivedВыведена из эксплуатации: не маршрутизирует новые тикеты, освобождает partial-unique слоты (ux_queues_routing_category, ux_queues_default).

Value objects

Value objects не используются (плоский справочник-конфиг).

Модели JSONB (IDocument, versioned)

JSONB-модели не используются.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
TicketEntityreference (children)tickets.queue_idвыборка/подсчёт по (TenantId, QueueId, Status)restrict (очередь с активными тикетами архивируется только после переноса — INV-Q-4)

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-Q-1В рамках TenantId Code уникаленуникальный индекс ux_queues_code + проверка при создании/обновлении очередиSUPPORT_QUEUE_CODE_DUPLICATE
INV-Q-2Ровно одна активная очередь с IsDefault = true; SEED гарантирует наличие, смена default — в одном согласованном изменении (снять со старой + поставить новой)уникальный partial-индекс ux_queues_default + назначение default-очередиSUPPORT_QUEUE_DEFAULT_REQUIRED / SUPPORT_QUEUE_DEFAULT_DUPLICATE
INV-Q-3RoutingCategory уникальна среди активных очередей (детерминированная маршрутизация: категория → не более одной очереди)уникальный partial-индекс ux_queues_routing_category + проверка при создании/обновленииSUPPORT_QUEUE_ROUTING_DUPLICATE
INV-Q-4Архивация запрещена, если в очереди есть активные тикеты (Status ∈ {New, InWork}) или это единственная активная default-очередь; перенос тикетов в другую очередь предшествует архивациипри архивации очередиSUPPORT_QUEUE_HAS_ACTIVE_TICKETS / SUPPORT_QUEUE_LAST_DEFAULT
INV-Q-5Маршрутизация при создании тикета: по RoutingCategory категории тикета → соответствующая активная очередь; при отсутствии совпадения → активная default-очередьпри маршрутизации создаваемого тикета— (детерминировано, default всегда существует по INV-Q-2)

Предусловия и постусловия переходов

ПереходИнициатор / условиеПредусловиеПостусловие (гарантируется)
Create ([*] → Active)супервизор (support.queues.manage)Code уникален в TenantId (INV-Q-1); при заданном RoutingCategory — категория не занята активной очередью (INV-Q-3); при IsDefault = true — прежняя default снимается в том же изменении (INV-Q-2)очередь Active, участвует в маршрутизации; поднято доменное QueueCreated
Update (Active → Active)супервизорCode иммутабелен; смена RoutingCategory/IsDefault соблюдает INV-Q-2/INV-Q-3новые routing/default-слоты заняты атомарно (старые освобождены), маршрутизация будущих тикетов идёт по новой конфигурации
Archive (Active → Archived)супервизорнет активных тикетов Status ∈ {New, InWork} (INV-Q-4, иначе SUPPORT_QUEUE_HAS_ACTIVE_TICKETS); не единственная активная default (иначе SUPPORT_QUEUE_LAST_DEFAULT)partial-unique слоты (ux_queues_routing_category, ux_queues_default) освобождены; очередь не маршрутизирует новые тикеты; поднято QueueArchived
Restore (Archived → Active)супервизорCode по-прежнему уникален; routing-категория и default-слот свободны (иначе SUPPORT_QUEUE_ROUTING_DUPLICATE / SUPPORT_QUEUE_DEFAULT_DUPLICATE)очередь снова Active и участвует в маршрутизации

Конкурентность и маршрутизация под нагрузкой

  • Смена default (INV-Q-2). «Снять флаг со старой + поставить новой» выполняется в одном согласованном изменении; две параллельные попытки назначить разные default-очереди сериализуются partial-unique индексом ux_queues_default — проигравшая упирается в SUPPORT_QUEUE_DEFAULT_DUPLICATE и повторяет на свежем состоянии. Промежуточного состояния «нет ни одной default» наблюдатель не видит (атомарность).
  • Занятие routing-категории (INV-Q-3). Гонка двух очередей за одну RoutingCategory разрешается тем же partial-unique индексом ux_queues_routing_categorySUPPORT_QUEUE_ROUTING_DUPLICATE проигравшему.
  • Архивация ↔ маршрутизация нового тикета. Маршрутизация (INV-Q-5) выбирает очередь среди Active; архивация переводит очередь в Archived. Если create-ticket уже вычислил QueueId этой очереди, а архивация фиксируется следом — тикет остаётся в архивируемой очереди, и её архивация отклоняется SUPPORT_QUEUE_HAS_ACTIVE_TICKETS (INV-Q-4: перед архивацией активных тикетов быть не должно). Супервизор переносит тикеты в другую активную очередь и повторяет архивацию; проигравшая по версии правка не затирает чужую (оптимистичная блокировка). Категория, оставшаяся без активной routing-очереди, детерминированно уходит в default General (INV-Q-5).

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
QueueCreatedдоменноеСоздание очередиId, TenantId, Code, RoutingCategory, IsDefault
QueueArchivedдоменноеПереход Active → ArchivedId, TenantId, Code, TargetQueueId?

Интеграционных Kafka-событий нет: очередь — внутренний справочник support, внешние контексты о ней не знают. Доменные события обрабатываются внутренними подписчиками в одном согласованном изменении.

SEED (Migrator)

Начальный набор очередей заводится SEED-миграцией Migrator-проекта support (read-only для API, меняется только миграцией). Гарантирует INV-Q-2 (наличие ровно одной активной default) и INV-Q-3 (уникальность routing-категорий).

CodeNameRoutingCategoryIsDefault
GeneralОбщиеnulltrue
DealsСделкиDealfalse
BillingФинансыPaymentfalse
ComplaintsЖалобыComplaintfalse
TechnicalТехническиеTechnicalfalse

Очередь «Сделки» обслуживает категорию Deal как routing-ключ; т.к. RoutingCategory единична (INV-Q-3), категория Booking при отсутствии выделенной активной очереди маршрутизируется в default General (INV-Q-5) — при необходимости заводится отдельная очередь или расширяется правило маршрутизации миграцией. Enum-значения категорий и приоритетов — ticket.md.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; ix_ticket_queues_tenant_id; ux_queues_code unique (tenant_id,code); ux_queues_routing_category unique partial where routing_category is not null and status='Active'; ux_queues_default unique partial where is_default and status='Active'; ix_ticket_queues_sort_order; ix_ticket_queues_status
Партиционирование / retentionnone (единицы строк на платформу); retention не применяется
Concurrencyоптимистическая блокировка — конфликты правки default/маршрутизации (правка не должна затирать чужую)
Permissions / PIIsupport.queues.manage — CRUD (супервизор поддержки); support.tickets.view/support.tickets.manage — read-доступ к очередям для фильтров/списков тикетов. PII отсутствует.
Auditотметки создания/изменения/удаления (кто и когда), soft-delete

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • ticket.mdQueueId, enum TicketCategory/TicketPriority/TicketStatus, правила маршрутизации create-ticket (INV-Q-5).
  • README — обзор контекста support, SEED-механика (Migrator), матрица 4.4.