06. Решения, открытые вопросы, план работ¶
6.1. Принятые решения (ADR)¶
ADR-001. Модульный монолит на Spring Modulith вместо набора сервисов¶
Контекст. Legacy: PHP site, Go menu, Go payment, Go user, Node notification.
Меню из iiko импортировалось дважды — в site/app/Models/Import/* и в menu — в две разные
схемы с разной моделью данных.
Решение. Один процесс, одна БД, модули с явными границами; границы держит Spring Modulith.
Следствия. Один импорт, одна модель данных, одна транзакция. Границы проверяются тестом
(ApplicationModules.verify()), а не договорённостями. Транзакционный outbox получаем из коробки
(Event Publication Registry) — событие «меню импортировано → пересобрать снапшот» не теряется при
падении процесса. Цена — общий деплой и требования Modulith к структуре пакетов.
ADR-011. JDK 25 и актуальный Spring Boot¶
Решение. JDK 25 (LTS), Spring Boot 4.x, Kotlin 2.x; версии проверяются на пустом каркасе в первый день. Обоснование. Виртуальные потоки без ограничений pinning — прямая выгода для IO-bound импорта (до 10 параллельных запросов к iiko) и для API витрины; generational ZGC и compact object headers уменьшают пиковую память при разборе 19 МБ JSON. Проект новый, легаси-зависимостей нет — брать устаревший рантайм не на чем.
ADR-002. Разделение слоёв «источник» и «витрина»¶
Решение. Схема iiko пишется только импортом; схема catalog — только админкой.
Переопределения — отдельные nullable-колонки, чтение через coalesce.
Следствия. Импорт не может затереть ручную работу; ручная работа не может исказить данные,
уходящие в заказ.
ADR-003. iiko — источник истины по цене, составу и наличию¶
Решение. Цены не редактируются в админке. Обоснование. Цена, по которой продано, должна совпадать с ценой в чеке. Любое переопределение на стороне сайта создаёт класс расхождений, который дороже гибкости. Следствия. Акции и скидки (этап 2) реализуются как отдельный слой поверх цены iiko со своими правилами, а не правкой цены товара.
ADR-004. Собственное дерево категорий витрины¶
Решение. catalog.category создаётся вручную; itemCategories и productCategories из iiko —
только сырьё.
Обоснование. Переименование категории в iiko («Роллы Доставка») не должно менять URL и SEO;
витрине нужны вложенность, картинки, тексты и порядок, которых в iiko нет.
ADR-005. Товар с вариантами, привязка вручную¶
Решение. Один товар = одна карточка, вариант = одна позиция iiko, привязку делает человек с
автоподсказкой.
Обоснование. В iiko размеры пиццы — 4 разных блюда в 4 разных категориях; автоматика по
названию ошибается на нестандартных наименованиях, а ошибка привязки означает неверную цену.
Инвариант. unique (iiko_item_size_id) — позиция iiko принадлежит максимум одному товару.
ADR-006. Модификаторы: состав и цена — на уровне блюда¶
Решение. iiko.item_modifier_group привязана к размеру блюда; цена модификатора хранится в
разрезе (группа, модификатор, меню, организация).
Обоснование (данные). Группа aee0fab6… встречается 163 раза с двумя разными наборами
позиций; 11 групп приходят без itemGroupId; три модификатора имеют разную цену в разных блюдах
(35 ₽ / 60 ₽). Схема legacy (modifier_prices по modifier_id + price_category + org) все три
факта теряет.
ADR-007. Один generic-товар вместо таблиц на каждый тип¶
Решение. Никаких dish_pizza / dish_snacks / dish_drinks / dish_non_food.
Различия типов выражаются данными: категория, ось вариативности, набор модификаторов, бейджи.
Обоснование. В legacy добавление типа товара = миграция + модель + репозиторий + экран +
ветка в сборке меню. Мерч и «пиво разливное» из выгрузки в такую модель не помещаются вовсе.
ADR-008. Снапшот меню вместо сборки на каждый запрос¶
Решение. Материализованный JSON на (организация, внешнее меню) в Postgres + Redis,
пересборка по событию, отдача с ETag.
Обоснование. Сборка меню — это join по 8 таблицам с 21 тыс. связок модификаторов; на каждый
запрос витрины это неприемлемо, а данные меняются несколько раз в сутки.
ADR-009. Админка — отдельное SPA¶
Решение. React+AntD собирается независимо, общается только через /api/admin/v1.
Обоснование. Orchid в legacy связал вёрстку админки с релизом бэкенда. Отдельное SPA
позволяет менять админку без деплоя бэкенда и наоборот, а также переиспользовать API для
интеграций.
ADR-012. Каталог проектируется и заполняется с нуля, legacy-данные не мигрируются¶
Контекст. В legacy товары разложены по таблицам dish_pizza, dish_snacks, dish_drinks,
dish_non_food с десятками таблиц-спутников (dish_pizza_ingredients, dish_pizza_dough,
dish_snack_sauces, dish_drink_toppings …), два параллельных дерева категорий и два импорта iiko.
Решение. Каталог не мигрируется: категории и карточки клиент собирает заново в новой админке,
изображения загружаются заново, заказы удаляются. Мигрируются только пользователи, и это
задача следующего этапа.
Обоснование. Перенос данных потянул бы за собой разбиение товаров по типам — ровно ту
структуру, от которой отказываемся (ADR-007). Экран «Новые позиции iiko» делает пересборку
обозримой: вся номенклатура лежит готовой очередью с автоподсказкой вариантов.
Следствия. Нужен список URL, важных для SEO, и редиректы. Legacy используется только как
источник разбора ошибок, не как источник данных и не как ориентир схемы.
ADR-013. Снимок привязки к позиции iiko¶
Контекст. Сотрудники случайно удаляют и пересоздают блюда в iiko; пересозданное блюдо приходит
с новым itemId. Ссылка варианта на позицию становится нерабочей, товар молча пропадает.
Решение. Вариант хранит слепок позиции iiko на момент привязки и последнее известное живое
состояние (catalog.variant_binding), пропажа порождает инцидент (catalog.binding_incident),
замена подбирается по SKU и названию, перепривязка — одно действие.
Обоснование. Карточка — это ручной труд: тексты, фото, SEO, сортировка. Ошибка в iiko не
должна стоить его повторения. Плюс появляется точный ответ на вопрос «почему товара нет на сайте».
Следствия. Удаление в iiko никогда не удаляет витринные сущности; окно ожидания
(missing_grace) отсекает шум от неполных выгрузок.
ADR-010. Публичное API отдаёт нормализованное меню¶
Решение. Модификаторы — отдельным словарём, товары ссылаются по id; группы с одинаковым составом схлопываются. Обоснование. 19 МБ ответа iiko при 243 блюдах — прямое следствие денормализации. Витрина должна получать сотни килобайт.
6.2. Открытые вопросы¶
ОВ-1. Мультибрендовость и франшиза — отложено, ждём уточнения от клиента¶
Клиент планирует франшизу, но модель не определена. Три сценария и их цена:
| Сценарий | Что означает | Влияние на модель |
|---|---|---|
| A. Франчайзи — организации в том же аккаунте iiko | одна номенклатура, одни карточки, отличаются цены и стоп-листы по организациям | изменений не требуется — текущая модель это уже умеет: цена и наличие в разрезе организации, видимость товара по организациям |
| B. Франчайзи — свой аккаунт iiko, общий бренд и каталог | свой apiLogin, свои организации, общие карточки |
нужна сущность «подключение к iiko» (iiko.connection) и connection_id в таблицах слоя iiko; витрина не меняется |
| C. Франчайзи — свой бренд, свой каталог и домен | всё своё: каталог, дизайн, домен | нужен brand_id почти во всех таблицах catalog, изоляция в API и правах, выбор бренда в админке |
Что делаем сейчас, чтобы не переделывать потом:
- Слой iiko проектируется с расчётом на несколько подключений: поле
connection_idвводится сразу со значением по умолчанию1,apiLoginберётся из конфигурации подключения, а не из глобальной настройки. Это дешёвая страховка, закрывающая сценарий B. - Витринные ключи (
slugкатегорий и товаров) уникальны глобально — при сценарии C уникальность станет составной(brand_id, slug), что является механической миграцией. - Публичное API уже принимает
organizationIdв каждом запросе — при любом сценарии точкой входа остаётся организация.
Вопрос клиенту, который нужно задать: будут ли у франчайзи собственные юрлица и собственные аккаунты iiko, или они заводятся организациями внутри существующего аккаунта? Ответ различает A/B (почти бесплатно) и C (значимая работа, лучше знать до начала реализации).
ОВ-2. Семантика startRevision в /api/2/menu/by_id¶
Отдаёт ли iiko при указании startRevision только изменения или полное меню. До проверки на
боевом ключе удаление позиций выполняется только на полном ночном импорте. Проверяется в первую
неделю разработки.
ОВ-3. Ценовые категории¶
В коде legacy — комментарий «в iiko изменилось API, теперь ценовая категория не обязательна» и хардкод соответствия меню → ценовая категория. Нужно подтвердить у клиента актуальную схему ценообразования: используется ли механизм ценовых категорий сейчас, или цены полностью определяются внешним меню и организацией.
ОВ-4. Поведение при стоп-листе¶
Скрывать товар или показывать неактивным с пометкой «нет в наличии»? Влияет на конверсию и на SEO (исчезающие товары). Предлагается настройка с умолчанием «показывать неактивным».
ОВ-5. Несколько внешних меню на организацию¶
В legacy их два (41188, 50865) — предположительно доставка и зал. Если витрина использует
только одно, второе не нужно синхронизировать. Требуется подтверждение, чтобы не гонять лишний
объём каждые 5 минут.
ОВ-6. Комбо и наборы¶
В выгрузке comboCategories пуст, но механизм в iiko есть. Если сеты («Сеты» есть в учётных
группах) собираются как комбо, потребуется отдельная модель. Сейчас предполагается, что сет —
обычное блюдо. Требуется подтверждение.
ОВ-7. Судьба legacy-сервисов¶
payment, user, notification — переносятся в монолит или остаются? На этап каталога не
влияет, но определяет план миграции. Предложение: перенести в монолит модулями на своих этапах,
notification может остаться отдельным (у него другой профиль нагрузки и внешние интеграции).
6.3. План работ по этапу «Каталог»¶
| # | Работа | Результат | Оценка |
|---|---|---|---|
| 0 | Инвентаризация размеров изображений (см. 07-media.md §7.1) — совместно с фронтендом витрины |
заполненная таблица мест использования и утверждённый список пресетов | 1–2 дн, блокирует п. 6 |
| 1 | Каркас монолита: Gradle, JDK 25, Spring Boot + Modulith, Flyway, Docker, CI, структура модулей, ApplicationModules.verify() |
собирается и деплоится «hello world» с миграциями и проверкой границ | 3–4 дн |
| 2 | Схема БД: iiko, catalog, media, platform |
миграции + тесты на Testcontainers | 3 дн |
| 3 | iiko-клиент: токен, организации, терминалы, ценовые категории, внешние меню | зелёные тесты на WireMock, данные в БД | 4 дн |
| 4 | Импорт меню: потоковый парсер, bulk-запись, ревизии, mark-and-sweep, предохранитель | полный импорт response.json ≤ 60 с, ≤ 512 МБ heap |
6–8 дн |
| 5 | Импорт стоп-листов + события + diff | стоп-лист на витрине ≤ 60 с | 2 дн |
| 5а | Снимки привязок и инциденты: детект пропаж, окно ожидания, подбор замены, перепривязка | сценарий «удалили блюдо в iiko → перепривязал в один клик» проходит на тестах | 3 дн |
| 6 | Модуль media: загрузка, S3, нормализация, нарезка по утверждённым пресетам, точка фокуса, дедупликация, перегенерация |
загрузка картинки, все пресеты, отдача через CDN, srcset в API |
4–5 дн |
| 7 | Домен каталога: категории, товары, варианты, размещения, публикация с правилами | покрытые тестами сценарии | 5 дн |
| 8 | Сборка снапшота + публичное API витрины | /api/v1/menu p95 ≤ 80 мс, ETag |
4 дн |
| 9 | Админский API + OpenAPI + RBAC + аудит | спецификация, по которой пишется фронт | 5 дн |
| 10 | Автоподсказка вариантов и триаж новых позиций | экран «Новые позиции iiko» работает | 3 дн |
| 11 | Админка: каркас SPA, авторизация, layout, генерация клиента | ходит в API, показывает /me |
3 дн |
| 12 | Админка: категории (дерево, DnD, формы, медиа) | экран готов | 4 дн |
| 13 | Админка: список товаров, фильтры, массовые операции | экран готов | 4 дн |
| 14 | Админка: карточка товара (все вкладки) | экран готов | 6–8 дн |
| 15 | Админка: новые позиции iiko, проблемы привязок, модификаторы, импорты, справочники | экраны готовы | 6 дн |
| 16 | Загрузка изображений в админке: кроп, точка фокуса, предпросмотр во всех пресетах, понятные ошибки валидации | экран, на котором невозможно «загрузить и не увидеть результат» | 3 дн |
| 17 | Нагрузочное тестирование, наблюдаемость, алерты, документация, рантбук | дашборд и рантбук | 3 дн |
| 18 | Первичное наполнение каталога вместе с клиентом (сопровождение, исправления по ходу) | каталог собран, витрина работает | 3 дн сопровождения |
Итого по этапу каталога: ≈ 68–76 человеко-дней. При двух разработчиках (бэкенд + фронт) с параллельной работой начиная с п. 9 — примерно 8–9 недель календарно.
Критический путь: п. 2 → 4 → 7 → 8 → 9 → 14. Работы 11–16 упираются в готовность спецификации (п. 9), поэтому OpenAPI фиксируется раньше реализации — фронт стартует на моках. Пункт 0 не на критическом пути, но блокирует п. 6 и 16, поэтому запускается в первый же день.
Наполнение каталога — работа клиента, а не разработки: примерно 250 позиций, из них ~80 пицц, собираемых в ~20 карточек с вариантами. При наличии готовых фотографий это несколько дней работы контент-менеджера, и это единственная цена отказа от миграции legacy-данных.
6.4. Что нужно от клиента для старта¶
- Доступ к боевому и тестовому
apiLoginiiko (для проверки ОВ-2 и ОВ-3). - Ответ по франшизе (ОВ-1).
- Подтверждение назначения внешних меню
41188и50865(ОВ-5). - Размеры изображений от фронтенда витрины — таблица
07-media.md§7.1. Блокирует нарезку. - Список организаций, которые должны быть на витрине в момент запуска.
- Список URL каталога, которые важно сохранить для SEO (каталог собирается заново, старые адреса автоматически не наследуются — нужны редиректы).
- Готовность контент-менеджера к первичному наполнению каталога и наличие фотографий блюд в исходном разрешении.
6.5. Пользователи из legacy¶
Единственные данные, которые переносятся. К этапу каталога отношения не имеют — переносятся вместе с модулем клиентов на следующем этапе. Что нужно решить заранее:
- как хранились пароли в legacy (алгоритм хеша) — от этого зависит, переживут ли пользователи миграцию без сброса пароля;
- авторизация по телефону с кодом (как сейчас) означает, что пароли могут быть вообще не нужны — тогда мигрируются телефон, имя, история адресов и бонусы;
- бонусы и накопления, если они хранятся в iiko, а не в legacy, вообще не мигрируются.
Заказы, по решению клиента, не переносятся.