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

Entity / Aggregate: LayoutEntity

Дом полей агрегата «Планировка» контекста chessboard. Проекции (карточка объекта недвижимости в catalog, правила вознаграждений rewards, эконом-модель по типам объектов недвижимости invest-analytics, измерение Layout в dev-stats) ссылаются сюда и описывают только дельту.

Назначение

LayoutEntity — типовая планировка объекта недвижимости внутри проекта: схемы (2D/3D-план), типовые площади, комнатность. Объекты недвижимости ссылаются на планировку (PropertyObject.LayoutId); планировка опционально входит в группу планировок (layout-group.md). На планировках строятся правила вознаграждений (rewards) и типизация объектов эконом-модели (invest-analytics), а dev-stats использует её как уровень фильтрации статистики (ADR-0057).

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

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

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKapp / Talent syncИдентификатор планировки.
TenantIdGuidДаindex ix_layouts_tenant_idappTenant boundary (ADR-0052).
ProjectIdGuidДаFK fk_layouts_projectchessboard.projects.id, indexappПроект-владелец (планировки типовые в рамках проекта).
LayoutGroupIdGuid?НетFK fk_layouts_layout_groupchessboard.layout_groups.id, index where not nullapp / Talent syncГруппа планировок (INV-LAY-3: группа того же проекта).
NamestringДаvarchar(128), min 1; unique ux_layouts_project_name (project_id, lower(name)) where deleted_at is nullTalent sync / appНазвание: «1К-А 32.5», «Студия S». Уникально в проекте (INV-LAY-2).
RoomsshortДаcheck rooms between 0 and 20; 0 = студияTalent sync / appТиповая комнатность планировки.
TotalAreadecimalДаnumeric(8,2), check total_area > 0Talent sync / appТиповая общая площадь, м².
LivingAreadecimal?Нетnumeric(8,2), check living_area > 0 and living_area <= total_areaTalent sync / appТиповая жилая площадь, м².
SchemasLayoutSchemasDocument (jsonb)Да (дефолт пустой)jsonb, typedTalent sync / appСхемы планировки (см. JSONB).
Descriptionstring?Нетvarchar(2000)appОписание планировки (PIN-поле).
PropertyObjectsCountintДа>= 0, default 0вычисляется (background project-aggregates-recalc)Денормализованный счётчик объектов недвижимости с этой планировкой.
ExternalTalentLayoutIdGuid?Нетunique ux_layouts_external_talent_layout_id (where not null)Talent syncId планировки в Talent Chessboard.
SyncSourceChessboardSyncSourceДаenum (текст), varchar(32)app / consumerPin / TalentChessboard (реплика-поля: Name, Rooms, площади, Schemas, LayoutGroupId-привязка).
ReplicaVersionlong?Нет (обязателен при реплике)>= 0Talent syncAnti-out-of-order guard (INV-LAY-4).
LastSyncedAtDateTimeOffset?НетTalent syncПоследний применённый синк.
CreatedAtDateTimeOffsetДаindex ix_layouts_created_atappДата создания.
CreatedByUserIdGuid?НетappКто создал.
UpdatedAtDateTimeOffsetДаappДата изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?Нетpartial-index filterappSoft-delete (архив планировки).
DeletedByUserIdGuid?НетappКто архивировал.
VersionlongДа>= 1appВерсия агрегата.

Модели JSONB (versioned)

LayoutSchemasDocument (jsonb, колонка schemas, ContractVersion = 1)

ПолеТипОбязательностьОграниченияSince version
ContractVersionintДа= 1v1
ItemsIReadOnlyList<LayoutSchemaModel>Да (может быть пустым)≤ 20 элементов; сортировка по SortOrderv1

Вложенная модель LayoutSchemaModel

ПолеТипОбязательностьОграниченияSince version
FileIdGuidДавнешний storage/CDNv1
KindLayoutSchemaKind (enum-string)ДаPlan2D / Plan3D / FloorPosition (положение на этаже) / Visualizationv1
Titlestring?Нетvarchar(128)v1
SortOrderintДа0..100v1

Unknown Kind из синка Talent маппится в Plan2D c warning-логом.

Связи

СвязьТипКлючDelete behavior
ProjectEntityreference (родитель)layouts.project_id → projects.idrestrict
LayoutGroupEntityreferencelayouts.layout_group_id → layout_groups.idrestrict (группа не архивируется при живых планировках)
PropertyObjectEntityreference (обратная)property_objects.layout_id → layouts.idrestrict — архив планировки запрещён при не удалённых объектах недвижимости (INV-LAY-5)

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

ИнвариантУсловиеКогда проверяетсяОшибка
INV-LAY-1 TenantFromProjectTenantId == Project.TenantIdпри созданииCHESSBOARD_LAYOUT_TENANT_MISMATCH
INV-LAY-2 UniqueNameInProjectName уникально среди не удалённых планировок проектапри создании и переименованииCHESSBOARD_LAYOUT_NAME_TAKEN
INV-LAY-3 GroupSameProjectLayoutGroup.ProjectId == ProjectId при привязкепри привязке к группеCHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH
INV-LAY-4 MonotonicReplicaСобытие синка — только при event.Version > ReplicaVersionпри применении события синкаидемпотентный пропуск
INV-LAY-5 ArchiveGuardАрхив (DeletedAt) запрещён при существующих не удалённых объектах недвижимости с LayoutId = Idпри архивированииCHESSBOARD_LAYOUT_HAS_PROPERTY_OBJECTS
INV-LAY-6 SyncOwnedFieldsРеплика-поля меняются только синком; PIN-поля — только локальнопри изменении полей (раздельные пути)CHESSBOARD_LAYOUT_FIELD_SYNC_MANAGED

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

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

ОперацияАктор / источникПредусловие (guard)ЭффектОтказ
Createсинк Talent / застройщикTenantId = Project.TenantId (INV-LAY-1); имя свободно в проекте (INV-LAY-2); привязка к группе того же проекта (INV-LAY-3); TotalArea > 0; LivingArea <= TotalAreaсоздана планировка, Version = 1, PropertyObjectsCount = 0CHESSBOARD_LAYOUT_TENANT_MISMATCH / CHESSBOARD_LAYOUT_NAME_TAKEN / CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH
Renameсинк (реплика-поле)новое имя свободно в проектеName обновлён, Version += 1CHESSBOARD_LAYOUT_NAME_TAKEN (при синке — суффикс (Talent) + warn, событие не теряется)
Reassign groupсинк / застройщикгруппа того же проекта (INV-LAY-3)LayoutGroupId обновлён; пересчёт LayoutsCount/Area затронутых групп — джоб recalcCHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH
Update schemas / rooms / площадисинк (реплика-поля)TotalArea > 0, LivingArea <= TotalArea, ≤ 20 схемполя обновлены, Version += 1значения вне диапазона синк корректирует (null/fallback + warn)
Update description (PIN)застройщикDescription обновлён (синком не перезаписывается, INV-LAY-6)
Archive (soft-delete)застройщикнет не удалённых объектов недвижимости с LayoutId = Id (INV-LAY-5, FK restrict)DeletedAt проставлен; исчезает из public-проекцийCHESSBOARD_LAYOUT_HAS_PROPERTY_OBJECTS
Restore (возврат из архива)застройщикимя снова свободно в проектеDeletedAt = nullCHESSBOARD_LAYOUT_NAME_TAKEN

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

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

СобытиеТипКогдаSnapshot/поля
LayoutCreatedEventдоменноеУспешное созданиеId, ProjectId
LayoutUpsertedKafkaEventинтеграционное (ContractVersion = 1, топик pin.chessboard.layout-upserted.v1, key = Id)Создание, обновление, архив (IsArchived = true)Snapshot: Id, TenantId, ProjectId, LayoutGroupId, Name, Rooms, TotalArea, LivingArea, PropertyObjectsCount, IsArchived, Version. Потребители: dev-stats (измерение Layout), rewards (условия правил по Layout), invest-analytics (типы объектов недвижимости), catalog

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

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

  • Ключ / версия. key = Id, ContractVersion = 1, монотонный Version; upsert-by-id, at-least-once.
  • Кто ожидает. dev-stats (измерение Layout), rewards (условия правил вознаграждений по Layout), invest-analytics (типизация объектов эконом-модели), catalog (карточка объекта недвижимости).
  • PropertyObjectsCount — производное. Считает джоб project-aggregates-recalc и переопубликует событие при изменении счётчика; downstream берут его из snapshot.
  • Архив. Архив планировки идёт снапшотом с IsArchived = true — потребители помечают запись архивной, а не удаляют (историческая привязка объектов/правил сохраняется).

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

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

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

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

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

  • layout-group.md — группа; property-object.md — объекты недвижимости планировки; project.md — родитель; reference-data.mdChessboardSyncSource.
  • Потребители снапшотов: docs/06-services/rewards/ (правила по Layout/LayoutGroup), docs/06-services/dev-stats/ (ADR-0057), docs/06-services/invest-analytics/.