01О проекте
Admister — мульти-вертикальный маркетплейс. Под одной крышей работают доска объявлений, авто с запчастями, недвижимость с продажей и арендой, товарный рынок с магазинами продавцов, цифровые товары, купонный сервис и прайс-агрегатор. У каждого свой поддомен, своя главная и своё представление о том, что такое «объявление».
Снаружи это семь разных продуктов. Внутри — одно приложение: сервис определяется по адресу запроса, а всё, что ниже — аккаунты, деньги, модерация, поиск, медиа, уведомления, — общее. Проект наш целиком: модель, движок, интерфейс и запуск каждой вертикали.
02Контекст и проблема
Обычный способ запустить несколько маркетплейсов — запустить несколько сайтов. Он работает до второго: появляются две таблицы пользователей, две очереди модерации, две интеграции с оплатой и два места, где одну и ту же ошибку нужно чинить дважды — и по-разному, потому что код к тому моменту уже разошёлся.
Обратная крайность не лучше. Сайт с категорией «Авто» не умеет спрашивать пробег, двигатель и номер так, как это делает авто-доска, а поиск недвижимости без этажа и числа комнат — не поиск недвижимости. Задача была в том, чтобы оставить один движок и при этом не делать вид, будто семь очень разных каталогов — это один каталог.
03Задачи проекта
- одна кодовая база и один аккаунт на все вертикали, сервис выбирается по адресу;
- свои атрибуты и фильтры у каждой вертикали: десятки полей у авто и недвижимости, и ни одного из них — в остальных сервисах;
- деньги, которым владелец доверяет: баланс, крипто-инвойсы, идемпотентное зачисление и сверка зависших платежей;
- причины возвращаться: сохранённые поиски с пушами, бейджи продавцов, аукционы и групповые закупки;
- два языка и четыре валюты показа без дублирования контента;
- мобильное приложение, которое ходит в свой BFF, а не разбирает страницы сайта.
04Что мы сделали
Реестр сервисов встроен в загрузку приложения. Приходит запрос, адрес сверяется с реестром — и дальше приложение знает, какая оно вертикаль: какие модули поднимать, какой набор атрибутов брать, какую главную и какое меню рисовать. Голый домен — доска объявлений, поэтому самый короткий адрес заодно и самый общий.
Всё, что не зависит от вертикали, написано один раз: аккаунты и сессии, баланс и платежи, приём медиа, модерация, сохранённые поиски, уведомления, карта сайта и API. Добавить седьмой сервис означало написать его атрибуты и его главную — а не ещё один маркетплейс.
05Семь сервисов — семь каталогов
- Объявления — общая доска: от велосипеда до дивана, публикация бесплатна.
- Авто — транспорт, номера и запчасти, со своим набором из нескольких десятков атрибутов и фильтрами поверх него.
- Недвижимость — продажа и аренда, свой набор атрибутов, карты и поиск по районам.
- Маркет — товары с магазинами продавцов: витрина на продавца, остатки, заказы.
- Цифровые товары — ключи, гифт-карты и eSIM: то, что доставляют, а не отправляют.
- Купоны — промокоды и акции магазинов-партнёров.
- Прайс — агрегатор цен: один товар в разных магазинах, сравнение и учтённые переходы.
У них общие аккаунт, баланс, очередь модерации и поисковый индекс — и ничего общего в атрибутах, ради чего всё и затевалось.
06Деньги: баланс, крипто-инвойсы, сверка
Платное — продвижение, тарифы магазинов, цифровые товары — оплачивается с внутреннего баланса, а баланс пополняется криптой через Payora, наш же шлюз. Тело инвойса собирается ровно в одной функции, потому что то же тело повторяет крон сверки под тем же ключом идемпотентности: одно разошедшееся поле вернулось бы конфликтом и подвесило платёж.
Две детали, которые мы повторим на любом шлюзе. В инвойс уходит язык самого покупателя из его строки в базе, а не из веб-запроса: у крона запроса нет, и иначе всем уходил бы английский. И адрес возврата ведёт в кабинет на голом домене — покупатель, начавший с авто-вертикали, возвращается залогиненным, а не на страницу «войдите снова».
07Доверие: модерация, бейджи, споры
Маркетплейс судят по худшему объявлению, поэтому модерация здесь не боковой экран. Каждое решение — одобрить, отклонить, снять — проходит через один обработчик, и «отклонить» намеренно не то же самое, что «снять»: первое — ответ автору с причиной, второе убирает то, что уже висело. Два глагола, два разных письма, два разных состояния в записи.
У продавцов и магазинов есть бейджи, которые зарабатывают, а не покупают: сколько живёт аккаунт, сколько сделок закрыто, подтверждены ли телефон и почта. Покупатель видит один и тот же бейдж везде, где встречает продавца, потому что считается он в одном месте.
08Поиск, сохранённые поиски и пуши
Поиск знает, в какой он вертикали. На авто-доске он предлагает поля, которые есть у машины; на недвижимости — комнаты, этаж и район; на общей доске остаётся нарочно простым. Подсказки приходят из отдельной точки, поэтому поле отвечает, пока страницу ещё читают.
Поиск можно сохранить — и именно это превращает посетителя в возвращающегося. Новые совпадения приходят веб-пушем, подписка принадлежит аккаунту, а не вкладке браузера, и тем же механизмом идут события, которых действительно ждут: аукцион вот-вот закроется, групповая закупка набрала порог.
09Мобильное приложение со своим бэкендом
Приложение не разбирает страницы сайта. Оно ходит в свой BFF: небольшой набор точек, отвечающих в тех формах, которые нужны экранам, с токенами, выдаваемыми отдельно от веб-сессий. Без токена открыты два пути — справочники, нужные до входа, и адрес живости.
Адрес живости отдельный намеренно. Реестр, который следит за флотом приложений, раньше спрашивал справочники, а они собирают все словари и курсы, — проверка живости не должна стоить дороже самой работы. Он же отдаёт версию договора: клиент, собранный под старую версию, виден раньше, чем это заметят его пользователи.
10SEO на семь фронтов
Семь сервисов на одном домене — это семь карт сайта, семь наборов структурированных данных и очень простой способ наплодить дубли. Каждая вертикаль публикует свою карту из кэша, страницы объявлений объявляют свой тип разметки, а страницы результатов поиска никогда не выдают себя за канонический адрес объявления.
Контент тоже на автомате: поток материалов приходит из Astrina — платформы, которую мы сделали ровно для этого, — и в момент прихода статьи кэш карты сайта её вертикали сбрасывается. Иначе новая страница ждала бы следующей пересборки, и никто бы не понял, почему её нет.
11Один канал наружу
Всё, что приложение отправляет наружу — вызовы API, вебхуки, партнёрские фиды, забираемые картинки, почта, — идёт через один модуль, привязанный к SOCKS5-прокси, и DNS резолвится через тот же прокси. Модуль работает fail-closed: недоступен прокси — запрос блокируется, а не уходит тихо с адреса сервера.
Это не украшение. Маркетплейс целый день тянет партнёрские фиды и чужие картинки, и каждый такой запрос иначе относил бы адрес площадки третьей стороне. Почта идёт тем же путём, через YourTrend: письма подписаны, а имя сервера не появляется в заголовках.
12Технологии
- PHP 8.3 без фреймворка, тонкая обёртка над PDO и подготовленные запросы везде;
- MariaDB, схема живёт нумерованными файлами миграций, а не в чьей-то памяти;
- медиа лежат вне вебрута и отдаются контроллером — загруженный файл никогда не становится исполняемым путём;
- сборка ассетов с хешем в имени и без инлайна того, что должно кэшироваться, с выключателем на случай сбоя;
- SOCKS5 наружу для каждого исходящего вызова, почта через SMTP YourTrend;
- Payora — крипто-инвойсы, Astrina — вход и материалы, Adgora — собственная реклама.
13Результат
Семь живых сервисов на одной кодовой базе, один аккаунт и один баланс на всех, и при этом атрибуты и фильтры различаются ровно настолько, насколько различаются каталоги. Новая вертикаль — это набор атрибутов и главная страница, а не новый проект, не вторая таблица пользователей и не вторая интеграция с оплатой.
Владельцу это даёт одно место, куда смотреть. Модерация, деньги, продавцы и контент ведутся из общей панели управления всей группы проектов, а маркетплейс отдаёт их подписанным админским API, а не вторым собственным админ-сайтом.
14Выводы
Вывод, который мы унесём в следующий маркетплейс: общим делают механизм, а не смысл. Аккаунты, деньги, модерация и поиск — одна и та же задача в каждой вертикали, и написать её нужно один раз. Атрибуты, фильтры и сама форма объявления — это и есть продукт, и попытка уложить их в одну универсальную таблицу делает маркетплейс неудобным сразу для всех.
Второй вывод — про деньги. Место, где описывается платёж, обязано быть единственной функцией: его всё равно повторит кто-то ещё — крон, повтор, сверка, — а два описания одного платежа это самый дешёвый способ тихо потерять деньги.
Если вы строите маркетплейс, доску объявлений или мульти-вертикальную площадку на одном движке — это работа, которую мы делаем целиком.



