Catalog Service — BR 3.4
Контракты
Что делаем
Миграция
-
src/main/resources/db/changelog/027_catalog_outbox.xml— таблицаcatalog_outbox:id uuid PK topic varchar(100) NOT NULL payload_json jsonb NOT NULL status varchar(20) NOT NULL default 'pending' -- pending / sent / failed attempts int NOT NULL default 0 next_attempt_at timestamp NOT NULL default now() last_error text NULL created_at timestamp NOT NULL default now() sent_at timestamp NULL INDEX (status, next_attempt_at) -- для worker'а
Kafka publisher
-
com.erp.catalog.entity.CatalogOutboxEntry+CatalogOutboxRepository -
com.erp.catalog.event.CatalogEventPublisherс методами:publishProductUpserted(Product product)— payload включает готовыйcategory_path(резолвим иерархию категорий до строки)publishProductDeleted(UUID productId, UUID franchiseId, LocalDateTime deletedAt)publishModifierGroupUpserted(ModifierGroup group)— payload: группа + опции +referenced_by_product_ids(список товаров ссылающихся на эту группу)publishModifierGroupDeleted(UUID groupId, UUID franchiseId, LocalDateTime deletedAt, List<UUID> referencedProductIds)
- Категории не публикуются как отдельные события — при правке категории Catalog Service каскадно публикует
publishProductUpsertedдля всех затронутых товаров (новыйcategory_path). - Каждый метод оборачивает событие в стандартную обёртку
{event_id, timestamp, version, payload}и пишет вcatalog_outboxв той же@Transactional. -
com.erp.catalog.worker.CatalogOutboxWorker—@Scheduled(fixedDelay = 5000):- Вычитывает
status=pending+next_attempt_at <= now()batch до 100 - Публикует в Kafka (
KafkaTemplate) - При успехе —
status=sent, sent_at=now - При ошибке —
attempts++, backoff (10s/30s/2m/10m), после 10 —status=failed
- Вычитывает
-
KafkaAdmin— декларация топиков (6 штук) с retention 7 дней (168 ч).
Hooks в существующих сервисах
-
ProductService.createProduct— послеsave():catalogEventPublisher.publishProductUpserted(product) -
ProductService.updateProduct— аналогично после save. -
ProductService.softDeleteProduct—publishProductDeleted(...). -
ProductService.restoreProduct—publishProductUpserted(...)(восстановление = upsert). -
CategoryService.createCategory/updateCategory/deleteCategory→ каскадно перепубликуем все затронутые товары. HelperpublishProductUpsertedForCategory(categoryId)— резолвит все товары этой категории + дочерних → publishProductUpserted для каждого. Отдельныхcatalog.category.*событий нет. -
ModifierGroupService.createGroup/updateGroup/updateOptions— публикацияcatalog.modifier_group.upsertedс payload группы + опций +referenced_by_product_ids(изproduct_modifier_groupsтаблицы связок). -
ModifierGroupService.softDelete—publishModifierGroupDeleted(...)сreferenced_by_product_ids.
Full snapshot endpoint
-
InternalCatalogController.getFullSnapshot(franchiseId)или новыйInternalCatalogSyncController:GET /internal/catalog/full-snapshot?franchise_id=X- Собирает
products(c готовымcategory_path) +modifier_groups. - Категории отдельно в ответе НЕ возвращаются —
category_pathуже зашит в каждый product. - Включая soft-deleted (поле
deleted_at). - Возвращает по контракту API.md.
- Собирает
- Rate limiting — bucket4j с Redis-backend. 1 запрос / 10 сек на
franchise_id. При превышении —429 RATE_LIMITED+Retry-After. - Auth — стандартный
X-Service-Token.
Per-product expand endpoint
-
InternalCatalogController.expandProduct(productId):GET /internal/catalog/products/{id}/expand- Возвращает развёрнутый список виртуальных PK-продуктов по правилу развёртывания:
base— если у товара нет структурных модификаторов; + N записейfree_addonдля каждого свободного модификатора × если есть структурный, то вариантов может быть M структурных × N addon’ов на каждый.structural_variant— по одному на каждую структурную опцию, имя с суффиксом опции, цена = base_price + структурная доплата.free_addon— по одному на каждую пару (структурный вариант × свободная опция), цена = цена свободной опции.
- Каждый variant включает
skuпо формуле{product_id}[:{struct_opt_id}][:+{free_opt_id}], полноеnameсcategory_pathпрефиксом.
- Возвращает развёрнутый список виртуальных PK-продуктов по правилу развёртывания:
- Хелпер
ProductExpansionService— чистая логика развёртывания, используется и для expand, и для full-snapshot (в snapshot’е товары возвращаются в оригинальной форме, но адаптер может использовать этот же хелпер локально).
Тесты
- Unit
CatalogEventPublisherTest— проверить payload соответствует контракту (обёртка + все поля). - Unit
CatalogOutboxWorkerTest— retry-логика, backoff. - Integration
ProductServiceITс embedded Kafka — послеcreateProductв outbox появляется запись, worker её публикует. - Integration
FullSnapshotIT— полный срез содержит все entity типы. - Test: rate-limit (11-й запрос в секунду → 429).
Документация
- Обновить
src/main/resources/README-events.md(если существует) или создать — описание published topics.
Зависимости
- Нет внешних — все изменения внутри Catalog Service.
- Kafka — используется существующий конфиг.
- Redis — уже есть для кэша (используем для bucket4j).
Deploy
После merge — стандартный передеплой catalog-service на VPS через /deploy-all catalog-service.