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

Entity / Aggregate: LayoutGroupEntity

Дом полей агрегата «Группа планировок» контекста chessboard. Проекции (правила вознаграждений rewards, уровень фильтрации LayoutGroup в dev-stats (ADR-0057), группировка в карточке проекта catalog) ссылаются сюда и описывают только дельту.

Назначение

LayoutGroupEntity — именованная группа типовых планировок проекта: «Студии», «1-комнатные», «Люксы с видом на море». Группа — уровень иерархии между проектом и планировкой: на неё настраиваются правила вознаграждений (rewards), по ней агрегируется статистика просмотров (dev-stats), она же — укрупнённый «тип объекта недвижимости» для отображения ассортимента.

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

ПолеЗначение
Контекстchessboard
KindAggregateRoot (гибрид с репликой Talent; справочная сущность)
Source of truthchessboard (PIN-поля) / talent.microservices.chessboard (реплика-поля)
Таблицаchessboard.layout_groups
Tenant isolationTenantId = DeveloperId проекта (ADR-0052)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKapp / Talent syncИдентификатор группы.
TenantIdGuidДаindex ix_layout_groups_tenant_idappTenant boundary (ADR-0052).
ProjectIdGuidДаFK fk_layout_groups_projectchessboard.projects.id, indexappПроект-владелец.
NamestringДаvarchar(128), min 1; unique ux_layout_groups_project_name (project_id, lower(name)) where deleted_at is nullTalent sync / appНазвание группы («Студии», «Люкс с видом на море»). Уникально в проекте (INV-LGR-2).
Descriptionstring?Нетvarchar(2000)appОписание группы (PIN-поле).
SortOrderintДа0..1000, default 0appПорядок отображения групп в ассортименте проекта.
LayoutsCountintДа>= 0, default 0вычисляется (background project-aggregates-recalc)Денормализованный счётчик планировок группы.
AreaNullableRange<decimal>Нетколонки area_min / area_max, numeric(8,2), area_max >= area_minвычисляется (background)Диапазон TotalArea планировок группы (Area.MinArea.Max, «от … м²» в ассортименте); обе границы null, если планировок в группе ещё нет.
ExternalTalentLayoutGroupIdGuid?Нетunique ux_layout_groups_external_talent_id (where not null)Talent syncId группы в Talent Chessboard.
SyncSourceChessboardSyncSourceДаenum (текст), varchar(32)app / consumerPin / TalentChessboard (реплика-поле: Name).
ReplicaVersionlong?Нет (обязателен при реплике)>= 0Talent syncAnti-out-of-order guard (INV-LGR-4).
LastSyncedAtDateTimeOffset?НетTalent syncПоследний применённый синк.
CreatedAtDateTimeOffsetДаindex ix_layout_groups_created_atappДата создания.
CreatedByUserIdGuid?НетappКто создал.
UpdatedAtDateTimeOffsetДаappДата изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?Нетpartial-index filterappSoft-delete (архив).
DeletedByUserIdGuid?НетappКто архивировал.
VersionlongДа>= 1appВерсия агрегата.

Value objects

AreaNullableRange<decimal>

Диапазон площадей планировок группы, хранится двумя границами area_min / area_max (обе nullable). Инвариант area_max >= area_min при обоих заполненных значениях; значения вычисляются фоновым процессом project-aggregates-recalc. Остальные поля группы атомарны.

Связи

СвязьТипКлючDelete behavior
ProjectEntityreference (родитель)layout_groups.project_id → projects.idrestrict
LayoutEntityreference (обратная)layouts.layout_group_id → layout_groups.idrestrict — архив группы запрещён при не архивированных планировках (INV-LGR-3)

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

ИнвариантУсловиеКогда проверяетсяОшибка
INV-LGR-1 TenantFromProjectTenantId == Project.TenantIdпри созданииCHESSBOARD_LAYOUT_GROUP_TENANT_MISMATCH
INV-LGR-2 UniqueNameInProjectName уникально среди не удалённых групп проектапри создании и переименованииCHESSBOARD_LAYOUT_GROUP_NAME_TAKEN
INV-LGR-3 ArchiveGuardАрхив запрещён при существующих не архивированных планировках группыпри архивированииCHESSBOARD_LAYOUT_GROUP_HAS_LAYOUTS
INV-LGR-4 MonotonicReplicaСобытие синка — только при event.Version > ReplicaVersionпри применении события синкаидемпотентный пропуск

Статусной state-машины нет: создана → архивирована через soft-delete.

Жизненный цикл и операции

ОперацияАктор / источникПредусловие (guard)ЭффектОтказ
Createсинк Talent / застройщикTenantId = Project.TenantId (INV-LGR-1); имя свободно в проекте (INV-LGR-2)создана группа, Version = 1, LayoutsCount = 0, Area обе границы nullCHESSBOARD_LAYOUT_GROUP_TENANT_MISMATCH / CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN
Renameсинк (реплика-поле Name)новое имя свободно в проектеName обновлён, Version += 1; при синке гонка версий отсекается ReplicaVersion-guard (INV-LGR-4)CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN
Update description / sort order (PIN)застройщикSortOrder в 0..1000поля обновлены, Version += 1 (синком не перезаписываются)
Archive (soft-delete)застройщикнет не архивированных планировок группы (INV-LGR-3, FK restrict)DeletedAt проставлен; снапшот IsArchived = trueCHESSBOARD_LAYOUT_GROUP_HAS_LAYOUTS
Restore (возврат из архива)застройщикимя снова свободно в проектеDeletedAt = nullCHESSBOARD_LAYOUT_GROUP_NAME_TAKEN

Постусловие мутаций: Version монотонно растёт; значимое изменение переопубликуется LayoutGroupUpsertedKafkaEvent; LayoutsCount/Area — денормализация, вычисляет джоб recalc (по составу планировок группы), а не операция над группой.

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

СобытиеТипКогдаSnapshot/поля
LayoutGroupCreatedEventдоменноеУспешное созданиеId, ProjectId
LayoutGroupUpsertedKafkaEventинтеграционное (ContractVersion = 1, топик pin.chessboard.layout-group-upserted.v1, key = Id)Создание, обновление, архив (IsArchived = true)Snapshot: Id, TenantId, ProjectId, Name, SortOrder, LayoutsCount, Area (NullableRange<decimal>), IsArchived, Version. Потребители: dev-stats (измерение LayoutGroup, ADR-0057), rewards (условия правил по LayoutGroup), catalog

Интеграционные события публикуются атомарно со сменой состояния агрегата.

Кросс-контекстный контракт (LayoutGroupUpsertedKafkaEvent)

  • Ключ / версия. key = Id, ContractVersion = 1, монотонный Version; upsert-by-id, at-least-once.
  • Кто ожидает. dev-stats (измерение LayoutGroup, ADR-0057), rewards (условия правил вознаграждений по LayoutGroup), catalog (группировка ассортимента в карточке проекта).
  • LayoutsCount/Area — производные. Пересчитывает джоб project-aggregates-recalc по составу планировок группы и переопубликует событие при их изменении; downstream берут из snapshot.
  • Архив. Идёт снапшотом с IsArchived = true — потребители помечают запись архивной, не удаляя (условия правил rewards/измерения dev-stats, ссылающиеся на группу, остаются историчны).

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

АспектЗначение
ИндексыPK pk_layout_groups; ux_layout_groups_project_name unique (project_id, lower(name)) where deleted_at is null; ux_layout_groups_external_talent_id unique where not null; ix_layout_groups_tenant_id; ix_layout_groups_project_id; ix_layout_groups_created_at
Партиционирование / retentionnone — групп сотни; retention бессрочный (soft-delete)
Concurrencyоптимистическая блокировка (правка не должна затирать чужую); для событий синка — guard по ReplicaVersion
Permissions / PIIPublic-чтение — группы published-проектов; CRUD — chessboard.layouts.manage (scope застройщика, общий с планировками); PII нет
Auditверсия агрегата + отметки кто/когда; события

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

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

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

  • layout.md — планировки группы; project.md — родитель; reference-data.mdChessboardSyncSource.
  • Потребители: docs/06-services/rewards/, docs/06-services/dev-stats/ (ADR-0057).