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

Синхронизация с mphone API

Что такое mphone

mphone — внешняя система управления складом поставщика. DM Shop синхронизирует из неё:

  • Категории товаров
  • Бренды
  • Товары (название, цена, остатки, атрибуты)

Архитектура интеграции

SyncServiceImpl (@Scheduled)

MphoneApiClient (rate-limited, paginated)

RestClient (Spring) + MphoneAuthInterceptor

mphone API / mock-api (localhost:8010)

Конфигурация

Все параметры задаются через @ConfigurationProperties("mphone.api"):

mphone:
api:
base-url: http://localhost:8010/api # или реальный URL
token: 30810FFF-9999-4FCE-B965-A920C70830BF
username: admin
password: secret
auth-type: TOKEN # TOKEN | BASIC
request-delay-ms: 2000 # задержка между запросами (rate limit)

Авторизация mphone

Поддерживаются два режима (auth-type):

  • TOKEN — заголовок Token: <token>
  • BASIC — заголовок Authorization: Basic <base64(user:pass)> + Token: <token>

В реальной системе используются оба заголовка одновременно (BASIC).

Алгоритм синхронизации

Метод SyncServiceImpl.run() запускается планировщиком каждые 30 секунд (fixedDelay=30_000).

run()
├── syncCategories()
│ 1. Читает last_sync_time из sync_state WHERE sync_type='CATEGORIES'
│ 2. Вызывает MphoneApiClient.listCategories(dateFrom=lastSyncTime)
│ 3. Для каждой категории: UPDATE или INSERT в categories по external_id
│ 4. Обновляет sync_state.last_sync_time

├── syncBrands()
│ (аналогично для таблицы brands)

└── syncProducts()
1. Читает last_sync_time из sync_state WHERE sync_type='PRODUCTS'
2. Постранично получает товары через MphoneApiClient.listProducts()
3. Для каждого товара:
- UPDATE или INSERT по external_id
- Синхронизирует атрибуты (product_attributes)
- Обновляет остатки (stock)
4. Обновляет sync_state.last_sync_time

Инкрементальная синхронизация

Таблица sync_state хранит время последней успешной синхронизации. При следующем запуске запрашиваются только изменения с dateFrom. Это снижает нагрузку на API.

При первом запуске (пустая sync_state) выполняется полная загрузка.

Пагинация запросов

MphoneApiClient автоматически перебирает страницы:

// Псевдокод пагинации
int page = 1;
do {
var response = restClient.get()
.uri("/ListProducts?page={page}&limit=100", page)
.retrieve().body(ListProductsResponse.class);
process(response.getItems());
page++;
} while (response.hasNextPage());

Между запросами выдерживается задержка requestDelayMs (по умолчанию 2000 мс) — это защита от rate-limiting на стороне mphone.

Ручное управление

Через API (только для ADMIN):

# Текущий статус синхронизации
GET /api/v1/admin/sync/states

# Принудительно запустить синхронизацию
POST /api/v1/admin/sync/run

# Сбросить состояние (следующий запуск сделает полную загрузку)
POST /api/v1/admin/sync/reset

Mock API для разработки

В локальном окружении вместо реального mphone используется mock-api/ (FastAPI на порту 8010).

# Сбросить и заполнить тестовыми данными
curl -X POST http://localhost:8010/dev/seed \
-H "Content-Type: application/json" \
-d '{"mode": "reset"}'

# Добавить товары
curl -X POST http://localhost:8010/dev/seed \
-H "Content-Type: application/json" \
-d '{"mode": "grow", "add_products": 500}'

Мок требует оба заголовка авторизации:

Authorization: Basic YWRtaW46c2VjcmV0
Token: 30810FFF-9999-4FCE-B965-A920C70830BF

Передача заказов в mphone

Функция обратная — заказ создаётся в DM Shop, затем передаётся в mphone:

AdminOrderController: POST /api/v1/admin/orders/{id}/submit
→ AdminOrderServiceImpl.submitToExternal(orderId)
→ MphoneApiClient.insertOrder(order)
→ OrderEntity.status = SENT (или ERROR если запрос не прошёл)
→ OrderEntity.externalId = <id из ответа mphone>

Десериализация дат mphone

mphone использует нестандартный формат даты: dd.MM.yyyy HH:mm:ss.SSS.

Для этого создан MphoneDateDeserializer — применяется не глобально (чтобы не сломать другие поля), а точечно через аннотацию:

@JsonDeserialize(using = MphoneDateDeserializer.class)
private LocalDateTime updatedAt;