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

Entity (Replica): MessengerUserEntity

Дом полей реплики identity (ФИО/телефон) контекста messenger — справочник пользователей для глобального поиска контактов по телефону, валидации адресата и display карточки контакта. Все проекции (результат поиска, маскированная/полная карточка профиля контакта, display участника чата) ссылаются сюда и перечисляют только дельту, а не переописывают поля.

Назначение

MessengerUserEntity — read-only проекция учётной записи платформы (source of truth — identity/domain/user.md) в контекст messenger. Держит ФИО и нормализованный телефон, чтобы мессенджер работал без синхронного запроса в identity:

  • глобальный поиск контакта по телефону — «Имя Отчество + первая буква Фамилии» до подтверждения контакта, операция search-users-by-phone;
  • валидация адресата заявки в контакты / личного сообщения (адресат существует и IsActive);
  • display контактов — полное ФИО + телефон раскрываются обеим сторонам только при Accepted (contact.md INV-CT-5), до этого фамилия маскируется.

Реплика наполняется по событию pin.identity.users.v1 с монотонным version-guard (алгоритм — consumers/identity-user-upserted.md; контракт события — identity/events/user-upserted.md). Собственных данных сущность не владеет и интеграционных событий не публикует (внутренняя реплика).

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

ПолеЗначение
Контекстmessenger
KindReplica (проекция из identity; наполняется только по событию, для пользовательских операций read-only)
Source of truthidentity (user.md) — owner ФИО/телефона
Таблицаmessenger.users
Tenant isolationнет — справочник person-level (глобальный поиск сквозной, ADR-0052). Id = UserId совпадает с identity.users.id (прямая адресация).

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
UserIdGuidДаPK (user_id)identity (= users.id)Ключ реплики и key события identity.
FirstNamestringДаvarchar(128), не пустоеidentity snapshotИмя (в поиске/маске отдаётся полностью).
MiddleNamestring?Нетvarchar(128)identity snapshotОтчество (в поиске/маске — полностью, если есть).
LastNamestringДаvarchar(128), допускается пустая строка при lazy-регистрацииidentity snapshotФамилия. В поиске до Accepted — только первая буква (INV-MU-3).
PhoneNumberstringДаvarchar(32), E.164; unique ux_users_phone (phone_number)identity snapshotКлюч поиска контактов и display; нормализуется (INV-MU-2).
IdentityUserVersionlong?Нетidentity (payload.Version)Монотонный guard применённых snapshot-ов (INV-MU-1); null — snapshot ещё не применялся.
IsActiveboolДаdefault trueidentity snapshotfalse, когда identity-статус Blocked/Archived — адресат недоступен для новых заявок/сообщений.
CreatedAtDateTimeOffsetДаconsumerПервое появление реплики.
UpdatedAtDateTimeOffsetДаconsumerМомент последнего применённого snapshot-а.

Value objects

ФИО зеркалит VO FullName из identity/domain/user.md, но в реплике хранится плоскими колонками FirstName/MiddleName/LastName — так работают проекция ФИО в Elasticsearch (по отдельным лексемам, search/contacts.md) и маскирование по LastName[0]. Единый дом концепта «ФИО» остаётся в identity — здесь только проекция.

FTS-поиск по ФИО/телефону (операция list-contacts) вынесен в Elasticsearch-проекцию MessengerContactSearchDocument (search/contacts.md, ADR-0059): в самой реплике полнотекстового индекса нет, PostgreSQL остаётся source of truth проекции.

Модели JSONB

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

Связи

СвязьТипКлючПравило загрузкиDelete behavior
UserEntity (identity)replica sourceUserId = identity.users.id (логическая)по событию pin.identity.users.v1— (в identity)
ContactEntityreference (обе стороны пары)RequesterUserId/AddresseeUserIdusers.user_id (логическая)при display карточки контактареплика не удаляется (IsActive=false)
ParticipantEntityreferenceparticipant.user_idusers.user_id (логическая)display участника чата
Адресат заявки/сообщенияvalidationUserIdguard IsActive при создании заявки/сообщения

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-MU-1Snapshot применяется только при payload.Version > IdentityUserVersion (last-write-wins по key = UserId; защита от out-of-order / replay)version-guard консюмера до примененияskip + метрика messenger_identity_stale_events_total
INV-MU-2PhoneNumber нормализуется к E.164 и уникален (ux_users_phone); конфликт — лог + тикет заботы, реплика не мутируетсянормализация в консюмере + unique-индексMSG_USER_PHONE_CONFLICT
INV-MU-3Поиск по телефону отдаёт маску фамилии (FirstName MiddleName LastName[0]+'.'), пока запрашивающий не в Accepted-контакте (или общем чате); при снятии маски раскрывается полное ФИО + телефон (INV-CT-5)проекция результата поиска / карточки профиля
INV-MU-4Реплика read-only для пользовательских операций — запись выполняет только консюмер identityотсутствие операций записи; доступны только чтение/поиск

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

Сущность не является агрегатом: доменных событий не поднимает, интеграционных Kafka-событий не публикует (внутренняя реплика; downstream реагирует на события identity).

Потребляет (наполнение реплики):

СобытиеТопикКогдаЭффект
UserUpsertedKafkaEventpin.identity.users.v1регистрация/обновление публичных полей пользователяupsert реплики с guard INV-MU-1; тот же коммит реиндексирует search-документ контактов (Elasticsearch, search/contacts.md)

Контракт события — identity/events/user-upserted.md; алгоритм консюмера — consumers/identity-user-upserted.md.

Окно консистентности и ожидания от identity. Реплика eventual: наполнение гарантирует at-least-once доставку user-upserted с ключом UserId, монотонным payload.Version и нормализованным E.164-телефоном. Только что зарегистрированный или переименованный пользователь становится находимым по телефону и адресуемым (заявка/сообщение) лишь после применения snapshot-а — обычно секунды, при лаге консюмера до минут. Пока snapshot не применён, операции контакта видят пользователя как несуществующего (MSG_CONTACT_USER_NOT_FOUND, contact.md INV-CT-6) — допустимая гонка, сходится после доставки. Смена IsActive тоже eventual: заявка/сообщение, начатые до применения блокировки в identity, могут пройти по ещё-активной реплике — guard IsActive есть точка на момент операции, а не распределённая транзакция.

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

АспектЗначение
ИндексыPK btree (user_id); ux_users_phone unique btree (phone_number). FTS по ФИО/телефону — Elasticsearch-проекция search/contacts.md (в PostgreSQL полнотекстового индекса нет, ADR-0059)
Партиционирование / retentionnone (справочник до ~1 млн пользователей, ADR-0056); строка живёт, пока жив identity-пользователь; архивация/блокировка в identityIsActive=false (не hard-delete)
Concurrencyоптимистической блокировки нет; порядок применения — монотонный IdentityUserVersion + key = UserId (last-write-wins, INV-MU-1)
Permissions / PIImessenger.contacts.search (поиск по телефону). PII: ФИО/телефон — класс PII; поиск отдаёт маску фамилии до Accepted (INV-MU-3); топик identity — m2m ACL Kafka (доверенные consumer-ы); в логах маскируется (+7***1234 / И***)
Auditactor-штампов нет (реплика); аудит источника — в identity (user.md)

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

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

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