🔗 Паттерны интеграции OZON Seller API
🔗 Паттерны интеграции OZON Seller API
Архитектурные решения и лучшие практики для интеграции с OZON Seller API.
📚 Содержание
- Архитектурные паттерны
- Управление состоянием
- Обработка ошибок
- Производительность
- Безопасность
- Примеры архитектур
🏗️ Архитектурные паттерны
1. Service Layer Pattern
Разделение бизнес-логики на отдельные сервисы для каждой области функциональности.
// Базовый класс сервиса
abstract class BaseService {
constructor(protected api: OzonSellerAPI) {}
protected async withRetry<T>(operation: () => Promise<T>, maxRetries: number = 3): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries) throw error;
await this.delay(Math.pow(2, attempt) * 1000);
}
}
throw new Error('All retries exhausted');
}
protected delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Сервис управления товарами
class ProductService extends BaseService {
async syncProduct(productData: ProductData): Promise<ProductSyncResult> {
return this.withRetry(async () => {
// 1. Проверяем существование товара
const existingProduct = await this.findExistingProduct(productData.sku);
if (existingProduct) {
// 2. Обновляем существующий товар
return await this.updateProduct(existingProduct.id, productData);
} else {
// 3. Создаем новый товар
return await this.createProduct(productData);
}
});
}
private async findExistingProduct(sku: string): Promise<Product | null> {
const products = await this.api.product.getList({
filter: { offer_id: [sku] },
limit: 1
});
return products.result?.items?.[0] || null;
}
private async createProduct(productData: ProductData): Promise<ProductSyncResult> {
const createResult = await this.api.product.create([{
name: productData.name,
offer_id: productData.sku,
category_id: productData.categoryId,
price: productData.price.toString(),
// ... остальные поля
}]);
const productId = createResult.result[0].product_id;
// Устанавливаем остатки
await this.api.pricesStocks.updateStocks([{
product_id: productId,
stock: productData.stock
}]);
return {
success: true,
productId,
action: 'created'
};
}
private async updateProduct(productId: number, productData: ProductData): Promise<ProductSyncResult> {
// Обновляем информацию о товаре
await this.api.product.updateInfo([{
product_id: productId,
name: productData.name,
// ... остальные поля
}]);
// Обновляем цену
await this.api.pricesStocks.updatePrices([{
product_id: productId,
price: productData.price.toString()
}]);
// Обновляем остатки
await this.api.pricesStocks.updateStocks([{
product_id: productId,
stock: productData.stock
}]);
return {
success: true,
productId,
action: 'updated'
};
}
}
// Сервис обработки заказов
class OrderService extends BaseService {
async processNewOrders(): Promise<OrderProcessingResult[]> {
const orders = await this.api.fbs.getOrdersList({
filter: { status: 'awaiting_packaging' },
limit: 100
});
const results: OrderProcessingResult[] = [];
for (const order of orders.result) {
try {
const result = await this.processOrder(order);
results.push(result);
} catch (error) {
results.push({
orderId: order.posting_number,
success: false,
error: error.message
});
}
}
return results;
}
private async processOrder(order: any): Promise<OrderProcessingResult> {
// 1. Проверяем доступность товаров
const availabilityCheck = await this.checkOrderAvailability(order);
if (!availabilityCheck.available) {
throw new Error(`Insufficient stock: ${availabilityCheck.message}`);
}
// 2. Упаковываем заказ
await this.api.fbs.packOrder({
posting_number: order.posting_number,
packages: [{
products: order.products.map(p => ({
product_id: p.product_id,
quantity: p.quantity
}))
}]
});
// 3. Генерируем трек-номер и отгружаем
const trackingNumber = await this.generateTrackingNumber();
await this.api.fbs.shipOrder({
posting_number: order.posting_number,
tracking_number: trackingNumber,
shipping_provider_id: 1
});
return {
orderId: order.posting_number,
success: true,
trackingNumber
};
}
private async checkOrderAvailability(order: any): Promise<AvailabilityCheck> {
// Логика проверки наличия товаров
return { available: true, message: '' };
}
private async generateTrackingNumber(): Promise<string> {
return `TRACK_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
2. Repository Pattern
Абстракция доступа к данным для упрощения тестирования и смены источников данных.
// Интерфейс репозитория
interface IProductRepository {
getById(id: number): Promise<Product | null>;
getByOfferIds(offerIds: string[]): Promise<Product[]>;
create(productData: CreateProductData): Promise<Product>;
update(id: number, updates: UpdateProductData): Promise<Product>;
updatePrices(priceUpdates: PriceUpdate[]): Promise<void>;
updateStocks(stockUpdates: StockUpdate[]): Promise<void>;
}
// Реализация для OZON API
class OzonProductRepository implements IProductRepository {
constructor(private api: OzonSellerAPI) {}
async getById(id: number): Promise<Product | null> {
try {
const response = await this.api.product.getInfo({ product_id: id });
return this.mapApiProductToProduct(response.result);
} catch (error) {
if (error.code === 'PRODUCT_NOT_FOUND') {
return null;
}
throw error;
}
}
async getByOfferIds(offerIds: string[]): Promise<Product[]> {
const response = await this.api.product.getList({
filter: { offer_id: offerIds },
limit: offerIds.length
});
return response.result?.items?.map(this.mapApiProductToProduct) || [];
}
async create(productData: CreateProductData): Promise<Product> {
const response = await this.api.product.create([{
name: productData.name,
offer_id: productData.offerId,
category_id: productData.categoryId,
price: productData.price.toString(),
// ... mapping остальных полей
}]);
const createdProductId = response.result[0].product_id;
// Получаем созданный товар
const product = await this.getById(createdProductId);
if (!product) {
throw new Error('Failed to retrieve created product');
}
return product;
}
async update(id: number, updates: UpdateProductData): Promise<Product> {
await this.api.product.updateInfo([{
product_id: id,
name: updates.name,
// ... mapping обновлений
}]);
const updatedProduct = await this.getById(id);
if (!updatedProduct) {
throw new Error('Failed to retrieve updated product');
}
return updatedProduct;
}
async updatePrices(priceUpdates: PriceUpdate[]): Promise<void> {
const apiUpdates = priceUpdates.map(update => ({
product_id: update.productId,
price: update.price.toString()
}));
await this.api.pricesStocks.updatePrices(apiUpdates);
}
async updateStocks(stockUpdates: StockUpdate[]): Promise<void> {
const apiUpdates = stockUpdates.map(update => ({
product_id: update.productId,
stock: update.stock
}));
await this.api.pricesStocks.updateStocks(apiUpdates);
}
private mapApiProductToProduct(apiProduct: any): Product {
return {
id: apiProduct.id,
offerId: apiProduct.offer_id,
name: apiProduct.name,
price: parseFloat(apiProduct.marketing_price || '0'),
stock: apiProduct.stocks?.coming || 0,
// ... mapping остальных полей
};
}
}
// Использование в сервисе
class ProductManagementService {
constructor(
private productRepo: IProductRepository,
private eventBus: EventBus
) {}
async syncProducts(externalProducts: ExternalProduct[]): Promise<SyncResult> {
const results = [];
for (const externalProduct of externalProducts) {
try {
const existingProduct = await this.productRepo.getById(externalProduct.id);
if (existingProduct) {
const updatedProduct = await this.productRepo.update(existingProduct.id, {
name: externalProduct.name,
price: externalProduct.price
});
results.push({ action: 'updated', product: updatedProduct });
this.eventBus.emit('product.updated', updatedProduct);
} else {
const newProduct = await this.productRepo.create({
name: externalProduct.name,
offerId: externalProduct.sku,
categoryId: externalProduct.categoryId,
price: externalProduct.price
});
results.push({ action: 'created', product: newProduct });
this.eventBus.emit('product.created', newProduct);
}
} catch (error) {
results.push({ action: 'error', error: error.message });
}
}
return { results, totalProcessed: externalProducts.length };
}
}
3. Event-Driven Architecture
Использование событий для слабосвязанной архитектуры и асинхронной обработки.
// Система событий
class EventBus {
private listeners: Map<string, Function[]> = new Map();
on(event: string, listener: Function): void {
const eventListeners = this.listeners.get(event) || [];
eventListeners.push(listener);
this.listeners.set(event, eventListeners);
}
emit(event: string, data: any): void {
const eventListeners = this.listeners.get(event) || [];
eventListeners.forEach(listener => {
// Асинхронный вызов для неблокирующей обработки
setImmediate(() => {
try {
listener(data);
} catch (error) {
console.error(`Error in event listener for ${event}:`, error);
}
});
});
}
}
// Типы событий
interface OrderEvents {
'order.created': { order: Order };
'order.shipped': { order: Order; trackingNumber: string };
'order.cancelled': { order: Order; reason: string };
}
interface ProductEvents {
'product.created': { product: Product };
'product.updated': { product: Product };
'product.stock.low': { product: Product; currentStock: number };
}
// Обработчики событий
class NotificationHandler {
constructor(private eventBus: EventBus) {
this.setupEventListeners();
}
private setupEventListeners(): void {
this.eventBus.on('order.shipped', this.handleOrderShipped.bind(this));
this.eventBus.on('product.stock.low', this.handleLowStock.bind(this));
}
private async handleOrderShipped(data: { order: Order; trackingNumber: string }): Promise<void> {
// Отправляем уведомление покупателю
await this.sendCustomerNotification(data.order, data.trackingNumber);
// Логируем событие
console.log(`Order ${data.order.id} shipped with tracking ${data.trackingNumber}`);
}
private async handleLowStock(data: { product: Product; currentStock: number }): Promise<void> {
// Отправляем алерт менеджеру
await this.sendStockAlert(data.product, data.currentStock);
}
private async sendCustomerNotification(order: Order, trackingNumber: string): Promise<void> {
// Реализация отправки уведомления
}
private async sendStockAlert(product: Product, stock: number): Promise<void> {
// Реализация отправки алерта
}
}
// Интеграция с основными сервисами
class OrderProcessingService {
constructor(
private api: OzonSellerAPI,
private eventBus: EventBus
) {}
async shipOrder(orderId: string, trackingNumber: string): Promise<void> {
const order = await this.getOrderById(orderId);
await this.api.fbs.shipOrder({
posting_number: orderId,
tracking_number: trackingNumber,
shipping_provider_id: 1
});
// Генерируем событие
this.eventBus.emit('order.shipped', { order, trackingNumber });
}
private async getOrderById(orderId: string): Promise<Order> {
// Реализация получения заказа
return {} as Order;
}
}
4. Command Pattern
Инкапсуляция операций в команды для лучшей организации кода и возможности отмены операций.
// Базовая команда
abstract class Command {
abstract execute(): Promise<any>;
abstract undo?(): Promise<void>;
}
// Команды для товаров
class CreateProductCommand extends Command {
constructor(
private api: OzonSellerAPI,
private productData: CreateProductData
) {
super();
}
async execute(): Promise<Product> {
const response = await this.api.product.create([{
name: this.productData.name,
offer_id: this.productData.offerId,
category_id: this.productData.categoryId,
price: this.productData.price.toString()
}]);
const productId = response.result[0].product_id;
// Сохраняем ID для возможной отмены
this.createdProductId = productId;
return {
id: productId,
name: this.productData.name,
offerId: this.productData.offerId,
price: this.productData.price
};
}
async undo(): Promise<void> {
if (this.createdProductId) {
await this.api.product.archive([this.createdProductId]);
}
}
private createdProductId?: number;
}
class UpdatePricesCommand extends Command {
constructor(
private api: OzonSellerAPI,
private priceUpdates: PriceUpdate[]
) {
super();
}
async execute(): Promise<void> {
// Сохраняем старые цены для возможной отмены
this.oldPrices = await this.getCurrentPrices(this.priceUpdates.map(u => u.productId));
const apiUpdates = this.priceUpdates.map(update => ({
product_id: update.productId,
price: update.price.toString()
}));
await this.api.pricesStocks.updatePrices(apiUpdates);
}
async undo(): Promise<void> {
if (this.oldPrices.length > 0) {
await this.api.pricesStocks.updatePrices(this.oldPrices);
}
}
private async getCurrentPrices(productIds: number[]): Promise<any[]> {
// Получаем текущие цены товаров
return [];
}
private oldPrices: any[] = [];
}
// Менеджер команд
class CommandManager {
private history: Command[] = [];
private currentIndex = -1;
async execute(command: Command): Promise<any> {
const result = await command.execute();
// Добавляем команду в историю
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(command);
this.currentIndex++;
return result;
}
async undo(): Promise<void> {
if (this.currentIndex >= 0) {
const command = this.history[this.currentIndex];
if (command.undo) {
await command.undo();
}
this.currentIndex--;
}
}
canUndo(): boolean {
return this.currentIndex >= 0;
}
getHistory(): Command[] {
return [...this.history];
}
}
// Использование
class ProductManagementFacade {
constructor(
private api: OzonSellerAPI,
private commandManager: CommandManager
) {}
async createProduct(productData: CreateProductData): Promise<Product> {
const command = new CreateProductCommand(this.api, productData);
return await this.commandManager.execute(command);
}
async updatePrices(priceUpdates: PriceUpdate[]): Promise<void> {
const command = new UpdatePricesCommand(this.api, priceUpdates);
await this.commandManager.execute(command);
}
async undoLastOperation(): Promise<void> {
if (this.commandManager.canUndo()) {
await this.commandManager.undo();
} else {
throw new Error('No operations to undo');
}
}
}
🔄 Управление состоянием
1. State Machine Pattern
Управление сложными жизненными циклами сущностей с четкими переходами состояний.
// Состояния заказа
enum OrderState {
AWAITING_PACKAGING = 'awaiting_packaging',
PACKAGING = 'packaging',
AWAITING_DELIVER = 'awaiting_deliver',
DELIVERING = 'delivering',
DELIVERED = 'delivered',
CANCELLED = 'cancelled'
}
// События заказа
enum OrderEvent {
PACK = 'pack',
SHIP = 'ship',
DELIVER = 'deliver',
CANCEL = 'cancel'
}
// Конфигурация состояний
const orderStateMachine = {
[OrderState.AWAITING_PACKAGING]: {
[OrderEvent.PACK]: OrderState.PACKAGING,
[OrderEvent.CANCEL]: OrderState.CANCELLED
},
[OrderState.PACKAGING]: {
[OrderEvent.SHIP]: OrderState.AWAITING_DELIVER,
[OrderEvent.CANCEL]: OrderState.CANCELLED
},
[OrderState.AWAITING_DELIVER]: {
[OrderEvent.DELIVER]: OrderState.DELIVERING,
[OrderEvent.CANCEL]: OrderState.CANCELLED
},
[OrderState.DELIVERING]: {
[OrderEvent.DELIVER]: OrderState.DELIVERED
},
[OrderState.DELIVERED]: {},
[OrderState.CANCELLED]: {}
};
// Менеджер состояний заказа
class OrderStateMachine {
constructor(
private api: OzonSellerAPI,
private eventBus: EventBus
) {}
async transition(order: Order, event: OrderEvent): Promise<Order> {
const currentState = order.state;
const allowedTransitions = orderStateMachine[currentState];
if (!allowedTransitions[event]) {
throw new Error(
`Invalid transition from ${currentState} with event ${event}`
);
}
const newState = allowedTransitions[event];
// Выполняем действия при переходе
await this.performTransitionActions(order, event, newState);
// Обновляем состояние заказа
const updatedOrder = { ...order, state: newState };
// Генерируем событие
this.eventBus.emit('order.state.changed', {
order: updatedOrder,
previousState: currentState,
event
});
return updatedOrder;
}
private async performTransitionActions(
order: Order,
event: OrderEvent,
newState: OrderState
): Promise<void> {
switch (event) {
case OrderEvent.PACK:
await this.packOrder(order);
break;
case OrderEvent.SHIP:
await this.shipOrder(order);
break;
case OrderEvent.CANCEL:
await this.cancelOrder(order);
break;
}
}
private async packOrder(order: Order): Promise<void> {
await this.api.fbs.packOrder({
posting_number: order.id,
packages: [{
products: order.products.map(p => ({
product_id: p.productId,
quantity: p.quantity
}))
}]
});
}
private async shipOrder(order: Order): Promise<void> {
const trackingNumber = this.generateTrackingNumber();
await this.api.fbs.shipOrder({
posting_number: order.id,
tracking_number: trackingNumber,
shipping_provider_id: 1
});
}
private async cancelOrder(order: Order): Promise<void> {
await this.api.cancellation.cancelFbsOrder({
posting_number: order.id,
cancel_reason_id: 352,
cancel_reason_message: 'Отмена по запросу'
});
}
private generateTrackingNumber(): string {
return `TRACK_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
2. Context Pattern
Управление контекстом операций с автоматической очисткой ресурсов.
// Контекст операции
class OperationContext {
private resources: Map<string, any> = new Map();
private cleanup: Function[] = [];
constructor(
private api: OzonSellerAPI,
private operationType: string
) {}
addResource(name: string, resource: any): void {
this.resources.set(name, resource);
}
getResource<T>(name: string): T | undefined {
return this.resources.get(name);
}
onCleanup(cleanupFn: Function): void {
this.cleanup.push(cleanupFn);
}
async dispose(): Promise<void> {
for (const cleanupFn of this.cleanup.reverse()) {
try {
await cleanupFn();
} catch (error) {
console.error('Error during cleanup:', error);
}
}
this.resources.clear();
this.cleanup.length = 0;
}
}
// Использование контекста
async function withOperationContext<T>(
api: OzonSellerAPI,
operationType: string,
operation: (context: OperationContext) => Promise<T>
): Promise<T> {
const context = new OperationContext(api, operationType);
try {
return await operation(context);
} finally {
await context.dispose();
}
}
// Пример использования
async function bulkUpdateProducts(
api: OzonSellerAPI,
products: ProductUpdate[]
): Promise<BulkUpdateResult> {
return withOperationContext(api, 'bulk-update-products', async (context) => {
// Создаем временный файл для логирования
const logFile = fs.createWriteStream('./bulk-update.log');
context.addResource('logFile', logFile);
context.onCleanup(() => logFile.close());
// Создаем прогресс-трекер
const progressTracker = new ProgressTracker(products.length);
context.addResource('progressTracker', progressTracker);
const results: UpdateResult[] = [];
for (const product of products) {
try {
const result = await updateSingleProduct(api, product);
results.push(result);
logFile.write(`SUCCESS: ${product.id}\n`);
progressTracker.increment();
} catch (error) {
results.push({
productId: product.id,
success: false,
error: error.message
});
logFile.write(`ERROR: ${product.id} - ${error.message}\n`);
progressTracker.increment();
}
}
return {
totalProcessed: products.length,
successful: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
results
};
});
}
🔧 Производительность
1. Batch Processing
Эффективная обработка больших объемов данных с помощью батчинга.
class BatchProcessor<T, R> {
constructor(
private batchSize: number = 1000,
private concurrency: number = 3,
private delayBetweenBatches: number = 1000
) {}
async process(
items: T[],
processor: (batch: T[]) => Promise<R[]>
): Promise<R[]> {
const batches = this.createBatches(items, this.batchSize);
const results: R[] = [];
// Обрабатываем батчи с ограничением параллелизма
for (let i = 0; i < batches.length; i += this.concurrency) {
const batchGroup = batches.slice(i, i + this.concurrency);
const batchPromises = batchGroup.map(async (batch, index) => {
try {
const batchResults = await processor(batch);
console.log(
`Batch ${i + index + 1}/${batches.length} completed: ${batch.length} items`
);
return batchResults;
} catch (error) {
console.error(`Batch ${i + index + 1} failed:`, error);
return [];
}
});
const batchGroupResults = await Promise.all(batchPromises);
results.push(...batchGroupResults.flat());
// Задержка между группами батчей
if (i + this.concurrency < batches.length) {
await this.delay(this.delayBetweenBatches);
}
}
return results;
}
private createBatches<T>(items: T[], batchSize: number): T[][] {
const batches: T[][] = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Использование для обновления цен
class PricingService {
constructor(private api: OzonSellerAPI) {}
async updatePricesBulk(priceUpdates: PriceUpdate[]): Promise<void> {
const batchProcessor = new BatchProcessor<PriceUpdate, void>(
1000, // размер батча
2, // параллелизм
500 // задержка между батчами
);
await batchProcessor.process(
priceUpdates,
async (batch) => {
const apiUpdates = batch.map(update => ({
product_id: update.productId,
price: update.price.toString()
}));
await this.api.pricesStocks.updatePrices(apiUpdates);
return []; // void результат
}
);
}
}
2. Caching Strategy
Многоуровневое кэширование для повышения производительности.
// Интерфейс кэша
interface ICache {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
}
// Реализация кэша в памяти
class MemoryCache implements ICache {
private cache = new Map<string, { value: any; expires: number }>();
async get<T>(key: string): Promise<T | null> {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expires) {
this.cache.delete(key);
return null;
}
return entry.value;
}
async set<T>(key: string, value: T, ttl: number = 300000): Promise<void> {
this.cache.set(key, {
value,
expires: Date.now() + ttl
});
}
async delete(key: string): Promise<void> {
this.cache.delete(key);
}
async clear(): Promise<void> {
this.cache.clear();
}
}
// Сервис с кэшированием
class CachedProductService {
constructor(
private api: OzonSellerAPI,
private cache: ICache
) {}
async getProduct(productId: number): Promise<Product> {
const cacheKey = `product:${productId}`;
// Пытаемся получить из кэша
const cached = await this.cache.get<Product>(cacheKey);
if (cached) {
return cached;
}
// Получаем из API
const response = await this.api.product.getInfo({ product_id: productId });
const product = this.mapApiProduct(response.result);
// Кэшируем на 5 минут
await this.cache.set(cacheKey, product, 300000);
return product;
}
async updateProduct(productId: number, updates: ProductUpdate): Promise<Product> {
// Обновляем через API
await this.api.product.updateInfo([{
product_id: productId,
...updates
}]);
// Инвалидируем кэш
await this.cache.delete(`product:${productId}`);
// Получаем обновленные данные
return this.getProduct(productId);
}
private mapApiProduct(apiProduct: any): Product {
return {
id: apiProduct.id,
name: apiProduct.name,
price: parseFloat(apiProduct.marketing_price || '0'),
// ... mapping остальных полей
};
}
}
3. Connection Pooling
Управление соединениями для оптимизации сетевых запросов.
// Менеджер соединений
class ConnectionPool {
private activeConnections = 0;
private queue: Array<{
resolve: Function;
reject: Function;
timeout: NodeJS.Timeout;
}> = [];
constructor(
private maxConnections: number = 10,
private queueTimeout: number = 30000
) {}
async acquire(): Promise<void> {
if (this.activeConnections < this.maxConnections) {
this.activeConnections++;
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
const index = this.queue.findIndex(item => item.resolve === resolve);
if (index >= 0) {
this.queue.splice(index, 1);
}
reject(new Error('Connection pool queue timeout'));
}, this.queueTimeout);
this.queue.push({ resolve, reject, timeout });
});
}
release(): void {
this.activeConnections--;
if (this.queue.length > 0) {
const next = this.queue.shift()!;
clearTimeout(next.timeout);
this.activeConnections++;
next.resolve();
}
}
getStats(): { active: number; queued: number; max: number } {
return {
active: this.activeConnections,
queued: this.queue.length,
max: this.maxConnections
};
}
}
// HTTP клиент с пулом соединений
class PooledOzonAPI {
private connectionPool: ConnectionPool;
constructor(
private api: OzonSellerAPI,
maxConnections: number = 10
) {
this.connectionPool = new ConnectionPool(maxConnections);
}
async makeRequest<T>(requestFn: () => Promise<T>): Promise<T> {
await this.connectionPool.acquire();
try {
return await requestFn();
} finally {
this.connectionPool.release();
}
}
async getProducts(params: any): Promise<any> {
return this.makeRequest(() => this.api.product.getList(params));
}
async updatePrices(updates: any[]): Promise<any> {
return this.makeRequest(() => this.api.pricesStocks.updatePrices(updates));
}
getConnectionStats() {
return this.connectionPool.getStats();
}
}
🛡️ Безопасность
1. API Key Management
Безопасное управление API ключами с ротацией и мониторингом.
// Интерфейс провайдера секретов
interface ISecretProvider {
getSecret(key: string): Promise<string>;
setSecret(key: string, value: string): Promise<void>;
rotateSecret(key: string): Promise<string>;
}
// Реализация для переменных окружения
class EnvironmentSecretProvider implements ISecretProvider {
async getSecret(key: string): Promise<string> {
const value = process.env[key];
if (!value) {
throw new Error(`Secret ${key} not found in environment`);
}
return value;
}
async setSecret(key: string, value: string): Promise<void> {
process.env[key] = value;
}
async rotateSecret(key: string): Promise<string> {
// В реальной реализации здесь была бы логика
// запроса нового ключа из внешней системы
const newSecret = await this.requestNewSecret(key);
await this.setSecret(key, newSecret);
return newSecret;
}
private async requestNewSecret(key: string): Promise<string> {
// Заглушка для демонстрации
return `new_secret_${Date.now()}`;
}
}
// Менеджер API ключей
class APIKeyManager {
private currentKeys: Map<string, string> = new Map();
private rotationSchedule: Map<string, NodeJS.Timeout> = new Map();
constructor(
private secretProvider: ISecretProvider,
private rotationIntervalHours: number = 24 * 7 // раз в неделю
) {}
async initialize(): Promise<void> {
// Загружаем ключи
const clientId = await this.secretProvider.getSecret('OZON_CLIENT_ID');
const apiKey = await this.secretProvider.getSecret('OZON_API_KEY');
this.currentKeys.set('clientId', clientId);
this.currentKeys.set('apiKey', apiKey);
// Планируем ротацию
this.scheduleRotation('apiKey');
}
getCredentials(): { clientId: string; apiKey: string } {
const clientId = this.currentKeys.get('clientId');
const apiKey = this.currentKeys.get('apiKey');
if (!clientId || !apiKey) {
throw new Error('API credentials not initialized');
}
return { clientId, apiKey };
}
private scheduleRotation(keyType: string): void {
const intervalMs = this.rotationIntervalHours * 60 * 60 * 1000;
const timeout = setTimeout(async () => {
try {
await this.rotateKey(keyType);
this.scheduleRotation(keyType); // Планируем следующую ротацию
} catch (error) {
console.error(`Failed to rotate ${keyType}:`, error);
// Повторяем попытку через час
setTimeout(() => this.scheduleRotation(keyType), 60 * 60 * 1000);
}
}, intervalMs);
this.rotationSchedule.set(keyType, timeout);
}
private async rotateKey(keyType: string): Promise<void> {
console.log(`Rotating ${keyType}...`);
const secretKey = keyType === 'apiKey' ? 'OZON_API_KEY' : 'OZON_CLIENT_ID';
const newKey = await this.secretProvider.rotateSecret(secretKey);
this.currentKeys.set(keyType, newKey);
console.log(`${keyType} rotated successfully`);
}
dispose(): void {
for (const timeout of this.rotationSchedule.values()) {
clearTimeout(timeout);
}
this.rotationSchedule.clear();
}
}
// Фабрика API с автоматической ротацией ключей
class SecureOzonAPIFactory {
constructor(private keyManager: APIKeyManager) {}
createAPI(): OzonSellerAPI {
const credentials = this.keyManager.getCredentials();
return new OzonSellerAPI({
clientId: credentials.clientId,
apiKey: credentials.apiKey,
// Другие настройки безопасности
timeout: 30000,
retryAttempts: 3
});
}
}
2. Request Validation
Валидация запросов и ответов для предотвращения ошибок.
// Схемы валидации
const productCreateSchema = {
type: 'object',
required: ['name', 'offer_id', 'category_id', 'price'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 500 },
offer_id: { type: 'string', minLength: 1, maxLength: 50 },
category_id: { type: 'number', minimum: 1 },
price: { type: 'string', pattern: '^\\d+\\.\\d{2}$' },
images: {
type: 'array',
items: { type: 'string', format: 'uri' },
maxItems: 15
}
}
};
// Валидатор
class RequestValidator {
private ajv: any;
constructor() {
// В реальном проекте используйте AJV или подобную библиотеку
this.ajv = null; // Заглушка
}
validate(schema: any, data: any): ValidationResult {
// Заглушка для демонстрации
const errors: string[] = [];
if (schema.required) {
for (const field of schema.required) {
if (!(field in data)) {
errors.push(`Missing required field: ${field}`);
}
}
}
return {
valid: errors.length === 0,
errors
};
}
}
// Обертка API с валидацией
class ValidatedOzonAPI {
constructor(
private api: OzonSellerAPI,
private validator: RequestValidator
) {}
async createProduct(productData: any): Promise<any> {
// Валидируем входные данные
const validation = this.validator.validate(productCreateSchema, productData);
if (!validation.valid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
// Выполняем запрос
const result = await this.api.product.create([productData]);
// Можем также валидировать ответ
this.validateResponse(result);
return result;
}
private validateResponse(response: any): void {
if (!response || typeof response !== 'object') {
throw new Error('Invalid API response format');
}
if (response.error) {
throw new Error(`API Error: ${response.error.message || 'Unknown error'}`);
}
}
}
interface ValidationResult {
valid: boolean;
errors: string[];
}
📱 Примеры архитектур
1. Микросервисная архитектура
// Базовый микросервис
abstract class BaseMicroservice {
protected api: OzonSellerAPI;
protected eventBus: EventBus;
protected logger: Logger;
constructor(config: MicroserviceConfig) {
this.api = new OzonSellerAPI(config.apiConfig);
this.eventBus = config.eventBus;
this.logger = config.logger;
}
abstract start(): Promise<void>;
abstract stop(): Promise<void>;
abstract getHealthStatus(): Promise<HealthStatus>;
}
// Сервис управления товарами
class ProductMicroservice extends BaseMicroservice {
private productService: ProductService;
constructor(config: MicroserviceConfig) {
super(config);
this.productService = new ProductService(this.api, this.eventBus);
}
async start(): Promise<void> {
this.logger.info('Starting Product Microservice...');
// Подписываемся на события
this.eventBus.on('external.product.updated', this.handleExternalProductUpdate.bind(this));
this.eventBus.on('inventory.stock.low', this.handleLowStock.bind(this));
this.logger.info('Product Microservice started');
}
async stop(): Promise<void> {
this.logger.info('Stopping Product Microservice...');
// Очистка ресурсов
}
async getHealthStatus(): Promise<HealthStatus> {
try {
// Проверяем доступность API
await this.api.product.getList({ limit: 1 });
return {
status: 'healthy',
timestamp: new Date().toISOString(),
details: {
apiConnection: 'ok',
eventBusConnection: 'ok'
}
};
} catch (error) {
return {
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: error.message
};
}
}
private async handleExternalProductUpdate(data: any): Promise<void> {
await this.productService.syncProduct(data.product);
}
private async handleLowStock(data: any): Promise<void> {
// Уведомляем о низком остатке
this.logger.warn(`Low stock for product ${data.productId}: ${data.currentStock}`);
}
}
// Сервис обработки заказов
class OrderMicroservice extends BaseMicroservice {
private orderService: OrderService;
private orderStateMachine: OrderStateMachine;
constructor(config: MicroserviceConfig) {
super(config);
this.orderService = new OrderService(this.api, this.eventBus);
this.orderStateMachine = new OrderStateMachine(this.api, this.eventBus);
}
async start(): Promise<void> {
this.logger.info('Starting Order Microservice...');
// Запускаем периодическую обработку заказов
setInterval(() => {
this.processNewOrders().catch(error => {
this.logger.error('Error processing orders:', error);
});
}, 60000); // каждую минуту
this.logger.info('Order Microservice started');
}
async stop(): Promise<void> {
this.logger.info('Stopping Order Microservice...');
}
async getHealthStatus(): Promise<HealthStatus> {
// Аналогично ProductMicroservice
return { status: 'healthy', timestamp: new Date().toISOString() };
}
private async processNewOrders(): Promise<void> {
const results = await this.orderService.processNewOrders();
this.logger.info(`Processed ${results.length} orders`);
for (const result of results) {
if (result.success) {
this.eventBus.emit('order.processed', { orderId: result.orderId });
} else {
this.eventBus.emit('order.processing.failed', {
orderId: result.orderId,
error: result.error
});
}
}
}
}
// Оркестратор микросервисов
class MicroserviceOrchestrator {
private services: BaseMicroservice[] = [];
private healthCheckInterval?: NodeJS.Timeout;
addService(service: BaseMicroservice): void {
this.services.push(service);
}
async startAll(): Promise<void> {
console.log('Starting all microservices...');
for (const service of this.services) {
await service.start();
}
// Запускаем мониторинг здоровья
this.startHealthChecks();
console.log('All microservices started');
}
async stopAll(): Promise<void> {
console.log('Stopping all microservices...');
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
}
for (const service of this.services.reverse()) {
await service.stop();
}
console.log('All microservices stopped');
}
private startHealthChecks(): void {
this.healthCheckInterval = setInterval(async () => {
for (const service of this.services) {
try {
const health = await service.getHealthStatus();
if (health.status === 'unhealthy') {
console.error(`Service unhealthy:`, health);
}
} catch (error) {
console.error('Health check failed:', error);
}
}
}, 30000); // каждые 30 секунд
}
}
2. Монолитная архитектура
// Главное приложение
class OzonSellerApplication {
private api: OzonSellerAPI;
private services: {
product: ProductService;
order: OrderService;
finance: FinanceService;
analytics: AnalyticsService;
};
private scheduler: JobScheduler;
private eventBus: EventBus;
constructor(config: ApplicationConfig) {
this.api = new OzonSellerAPI(config.apiConfig);
this.eventBus = new EventBus();
this.services = {
product: new ProductService(this.api, this.eventBus),
order: new OrderService(this.api, this.eventBus),
finance: new FinanceService(this.api, this.eventBus),
analytics: new AnalyticsService(this.api, this.eventBus)
};
this.scheduler = new JobScheduler();
this.setupEventHandlers();
this.scheduleJobs();
}
async start(): Promise<void> {
console.log('Starting OZON Seller Application...');
// Инициализация сервисов
await Promise.all([
this.services.product.initialize(),
this.services.order.initialize(),
this.services.finance.initialize(),
this.services.analytics.initialize()
]);
// Запуск планировщика
this.scheduler.start();
console.log('Application started successfully');
}
async stop(): Promise<void> {
console.log('Stopping application...');
this.scheduler.stop();
await Promise.all([
this.services.product.cleanup(),
this.services.order.cleanup(),
this.services.finance.cleanup(),
this.services.analytics.cleanup()
]);
console.log('Application stopped');
}
private setupEventHandlers(): void {
// Продукты
this.eventBus.on('product.created', this.handleProductCreated.bind(this));
this.eventBus.on('product.stock.low', this.handleLowStock.bind(this));
// Заказы
this.eventBus.on('order.shipped', this.handleOrderShipped.bind(this));
this.eventBus.on('order.cancelled', this.handleOrderCancelled.bind(this));
// Финансы
this.eventBus.on('payment.received', this.handlePaymentReceived.bind(this));
}
private scheduleJobs(): void {
// Синхронизация товаров каждые 15 минут
this.scheduler.addJob('sync-products', '*/15 * * * *', async () => {
await this.services.product.syncAllProducts();
});
// Обработка заказов каждые 5 минут
this.scheduler.addJob('process-orders', '*/5 * * * *', async () => {
await this.services.order.processNewOrders();
});
// Генерация отчетов каждый день в 9:00
this.scheduler.addJob('daily-report', '0 9 * * *', async () => {
await this.services.analytics.generateDailyReport();
});
}
private async handleProductCreated(data: any): Promise<void> {
// Логика обработки создания товара
await this.services.analytics.trackProductCreation(data.product);
}
private async handleLowStock(data: any): Promise<void> {
// Отправляем уведомление о низком остатке
console.log(`Low stock alert: Product ${data.productId} has ${data.stock} items left`);
}
private async handleOrderShipped(data: any): Promise<void> {
// Обновляем аналитику
await this.services.analytics.trackOrderShipment(data.order);
}
private async handleOrderCancelled(data: any): Promise<void> {
// Логика обработки отмены заказа
await this.services.analytics.trackOrderCancellation(data.order);
}
private async handlePaymentReceived(data: any): Promise<void> {
// Обработка получения платежа
await this.services.finance.processPayment(data.payment);
}
// Публичные методы для внешнего использования
getProductService(): ProductService {
return this.services.product;
}
getOrderService(): OrderService {
return this.services.order;
}
getFinanceService(): FinanceService {
return this.services.finance;
}
getAnalyticsService(): AnalyticsService {
return this.services.analytics;
}
}
// Планировщик задач
class JobScheduler {
private jobs: Map<string, { cron: string; handler: Function; running: boolean }> = new Map();
private intervals: Map<string, NodeJS.Timeout> = new Map();
addJob(name: string, cronExpression: string, handler: Function): void {
this.jobs.set(name, {
cron: cronExpression,
handler,
running: false
});
}
start(): void {
for (const [name, job] of this.jobs) {
this.scheduleJob(name, job);
}
}
stop(): void {
for (const interval of this.intervals.values()) {
clearInterval(interval);
}
this.intervals.clear();
}
private scheduleJob(name: string, job: { cron: string; handler: Function; running: boolean }): void {
// Упрощенная реализация - в реальном проекте используйте node-cron
const intervalMs = this.parseCronToInterval(job.cron);
const interval = setInterval(async () => {
if (job.running) {
console.log(`Job ${name} is already running, skipping...`);
return;
}
job.running = true;
try {
console.log(`Executing job: ${name}`);
await job.handler();
console.log(`Job ${name} completed successfully`);
} catch (error) {
console.error(`Job ${name} failed:`, error);
} finally {
job.running = false;
}
}, intervalMs);
this.intervals.set(name, interval);
}
private parseCronToInterval(cron: string): number {
// Упрощенный парсер cron - только для демонстрации
if (cron === '*/5 * * * *') return 5 * 60 * 1000; // 5 минут
if (cron === '*/15 * * * *') return 15 * 60 * 1000; // 15 минут
if (cron === '0 9 * * *') return 24 * 60 * 60 * 1000; // 24 часа
return 60 * 1000; // По умолчанию 1 минута
}
}
📚 Заключение
Представленные паттерны интеграции помогают создавать масштабируемые, надежные и поддерживаемые решения для работы с OZON Seller API. Выбор конкретного паттерна зависит от требований проекта, размера команды и архитектурных предпочтений.