Синхронизация с 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;