Единая база не создаётся принудительно. Она вырастает через операции. Контрагент начинает как локальный и глобализируется только при необходимости.
Глобальная база знает КТО, но не знает СКОЛЬКО
Один человек — один UID — каждый обменник видит по-своему: Глобальный реестр Тенант Альфы Тенант Беты ┌──────────────────┐ │ FO-7a3f8b2c4d1e │──────> PartnerEntity PartnerEntity │ user_id: 15 │ name: "Вася" name: "Василий П." │ phone: +7... │ type: client type: client │ │ memo: "VIP" memo: "мелкий" └──────────────────┘ balance: USD +5K balance: RUB +150K
Как адрес кошелька в блокчейне: глобально уникальный, короткий, проверяемый
Контрагент попадает в глобальную базу не при создании, а при первой операции, где нужна глобальность
Клиент зарегистрировался на платформе и связал свой аккаунт с карточкой у обменника
Обменник создал заявку с участием этого клиента для другого брокера
Клиент отправляет деньги на UID другого контрагента — оба должны быть глобальными
Обменник вручную "опубликовал" контрагента в сеть
Клиент зарегистрировался на платформе (получил user_id) и связал его с существующей карточкой у обменника.
// Клиент нажал "Это мой обменник" → подтвердил по SMS GlobalPartnerService::register(user_id: 15, phone: "+7925...") → GlobalPartnerEntity создаётся (uid = FO-7a3f8b2c4d1e) → PartnerEntity.global_uid = "FO-7a3f8b2c4d1e" → UserPartnerLinkEntity создаётся (user → broker → partner) // Теперь клиент видит свой баланс у этого обменника в ЛК
Система требует UID для создания межброкерской заявки → автоматическое создание GlobalPartnerEntity.
// Альфа создаёт поручение для кассира Беты: "выдать Васе $500" OrderEntity: counterpart_uid: ??? ← нужен UID, но Вася локальный! executor_broker_id: Бета type: payout amount: $500 Система автоматически: 1. Создаёт GlobalPartnerEntity для Васи 2. PartnerEntity.global_uid = новый UID 3. Заполняет counterpart_uid в OrderEntity
Если клиент переводит средства на UID другого к/а, оба участника должны быть в глобальной базе.
// "Переведи $5K с Васи на FO-2b9c1d4e8f3a" TransactionEntity: global_uid_from: FO-7a3f8b2c4d1e ← Вася должен быть глобальным global_uid_to: FO-2b9c1d4e8f3a instrument: USD-CASH amount: 5000.00 Если Вася ещё локальный: → Промоция → GlobalPartnerEntity → UID → транзакция
Обменник решил опубликовать контрагента в сеть — например, для совместной работы с другими брокерами.
// Обменник в интерфейсе нажал "Опубликовать в сеть" GlobalPartnerService::publish(partner_id: 847, broker_id: 1) → Проверка: есть ли у к/а телефон или telegram_id → Создаёт GlobalPartnerEntity → PartnerEntity.global_uid = новый UID → К/а теперь доступен для межброкерских операций // Другие брокеры могут найти его по UID или телефону
От локальной записи до мульти-брокерского клиента
Создана PartnerEntity (локальный, global_uid = null). Вася существует только у этого обменника. Достаточно для внутренних операций.
Создана GlobalPartnerEntity (uid = FO-7a3f8b2c4d1e). PartnerEntity получает global_uid. Создана UserPartnerLinkEntity — связь user → broker → partner.
Бета вводит UID или телефон → платформа находит FO-7a3f8b2c4d1e → создаёт новую PartnerEntity в тенанте Беты с тем же global_uid. Баланс у Беты = 0 (новый счёт). Один UID, два независимых баланса.
Клиентский портал через UserPartnerLinkEntity собирает балансы со всех тенантов. Вася видит: Альфа (USD $5K, EUR €3K) + Бета (RUB ₽150K) в одном интерфейсе.
Как по UID найти конкретную запись в конкретном тенанте
FO-7a3f8b2c4d1e → в какой базе искать → local_partner_id.
// Мастер-БД (fastotc) — знает UID → broker → local_id GlobalPartnerEntity (таблица: exchange_global_partner) uid FO-7a3f8b2c4d1e — PK, уникальный user_id 15 — FK → UserEntity (nullable) phone_hash sha256(+7925...) — для дедупликации telegram_id @vasya — для резолва из мессенджеров created_at datetime UserPartnerLinkEntity (таблица: exchange_user_partner_link) user_id 15 — FK → UserEntity broker_id 1 — FK → BrokerEntity partner_id 847 — id в тенантной базе // Per-broker DB — знает всё о к/а у ЭТОГО брокера PartnerEntity (таблица: crm_partner) + новое поле: uid VARCHAR, indexed — копия из мастер-БД, nullable
| Операция | Где ищем | Как |
|---|---|---|
| Найти к/а по UID | Мастер-БД | GlobalPartnerEntity::findByUid('FO-...') |
| Найти все связи с брокерами | Мастер-БД | UserPartnerLinkEntity::findByGlobalUid() |
| Найти local_partner_id у Беты | Мастер-БД | WHERE user_id=15 AND broker_id=2 → partner_id |
| Быстрый поиск внутри тенанта | Per-broker DB | PartnerEntity::findByUid('FO-...') — по indexed полю |
| Дедупликация при регистрации | Мастер-БД | GlobalPartnerEntity::findByPhoneHash() |
Жёсткие границы — обменники не видят финансы друг друга
Один UID — один интерфейс — балансы у всех обменников
UserPartnerLinkEntity::findByUserId(15) → [{broker:1, partner:847}, {broker:2, partner:12}]UID — общий язык. Обменникам не нужно знать внутренние id друг друга.
OrderEntity: counterpart_uid = FO-7a3f8b2c4d1e, executor_broker_id = Бета, type = payout, instrument = USD-CASH, amount = $500
"Выдать $500 контрагенту FO-7a3f8b2c4d1e" → ищет у себя PartnerEntity с этим uid
Нашла → "Выдать $500 Василию". Не нашла → автосоздание PartnerEntity с global_uid, name из GlobalPartnerEntity
Кассир Беты выдаёт $500 → TransactionEntity в тенанте Беты + ObligationEntity: debtor=Альфа, creditor=Бета, $500
// Хэши строятся через UID, не локальные id TransactionEntity.tx_hash = SHA256( global_uid_from: FO-7a3f8b2c4d1e global_uid_to: FO-2b9c1d4e8f3a instrument: USD-CASH amount: 500.00 timestamp: 2026-04-12T14:30:15Z prev_hash: tx-предыдущий ) // Преимущества UID в хэшах: // — Верифицируется из любого тенанта // — Арбитр сопоставляет операции из разных тенантов // — Переименование к/а не ломает хэш
Нажимайте кнопки и наблюдайте, как контрагенты глобализируются
Что уже создано в коде и что осталось
Таблица exchange_global_partner. PK = uid (FO-xxx). Поля: user_id, phone, telegram_id, created_at.
Связь user → broker → partner_id в тенанте. Кросс-тенантная навигация.
generate() — FO-{12hex}{CRC8}, validate(), normalize(). Crypto-random.
register() (идемпотентно), findByUid/Phone/UserId(), linkUserToPartner(), confirmLink().
Добавить nullable VARCHAR + index в существующую crm_partner. Синхронизация с мастер-БД.
Автоматическая глобализация при 4 событиях. Event listeners / Domain Events.
Агрегация балансов с нескольких тенантов. Multi-context query service.
Гарантия, что межброкерский API не отдаёт финансовые данные чужого тенанта.
Контрагенты не загружаются в глобальную базу массово. Каждый начинает локально и промоутится только при реальной потребности. Нет давления на обменника.
GlobalPartnerEntity хранит только UID, user_id, телефон. Вся бизнес-информация остаётся в тенанте. Это упрощает GDPR и снижает риски утечки.
"Переведи $5K на FO-7a3f8b2c4d1e" — однозначно. Хэши транзакций строятся на UID, не на локальных id. Арбитраж между брокерами возможен.
Брокер приглашает клиента → клиент видит удобный ЛК → сам становится брокером. Каждый GlobalPartnerEntity — потенциальный платящий клиент платформы.