πŸš€ Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с популярными Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ°ΠΌΠΈ

Π“ΠΎΡ‚ΠΎΠ²Ρ‹Π΅ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ OZON Seller API SDK с популярными JavaScript/TypeScript Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ°ΠΌΠΈ ΠΈ ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ°ΠΌΠΈ.

πŸ“‹ Π‘ΠΎΠ΄Π΅Ρ€ΠΆΠ°Π½ΠΈΠ΅


Next.js (React)

Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°

src/
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ ProductManager.tsx
β”‚   └── OrderProcessor.tsx
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ ozon-client.ts
β”‚   └── types.ts
β”œβ”€β”€ pages/
β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”œβ”€β”€ products/
β”‚   β”‚   └── orders/
β”‚   └── dashboard/
└── hooks/
    └── useOzonAPI.ts

ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°

// src/lib/ozon-client.ts
import { OzonSellerAPI } from 'bmad-ozon-seller-api';

const api = new OzonSellerAPI({
  clientId: process.env.OZON_CLIENT_ID!,
  apiKey: process.env.OZON_API_KEY!,
  timeout: 30000,
  debug: process.env.NODE_ENV === 'development'
});

export { api };

// Π’ΠΈΠΏΡ‹ для Next.js
export interface OzonConfig {
  clientId: string;
  apiKey: string;
  baseURL?: string;
}

React Hook для API

// src/hooks/useOzonAPI.ts
import { useState, useEffect } from 'react';
import { api } from '@/lib/ozon-client';

export function useOzonAPI() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleRequest = async <T>(
    request: () => Promise<T>
  ): Promise<T | null> => {
    setLoading(true);
    setError(null);
    
    try {
      const result = await request();
      return result;
    } catch (err: any) {
      setError(err.message || 'ΠŸΡ€ΠΎΠΈΠ·ΠΎΡˆΠ»Π° ошибка');
      return null;
    } finally {
      setLoading(false);
    }
  };

  return { api, loading, error, handleRequest };
}

// ΠŸΡ€ΠΈΠΌΠ΅Ρ€ использования Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅
export function useProducts() {
  const { api, loading, error, handleRequest } = useOzonAPI();
  
  const getProducts = (filters: any) => {
    return handleRequest(() => api.product.getList(filters));
  };
  
  const updateStock = (updates: any[]) => {
    return handleRequest(() => api.pricesStocks.updateStocks(updates));
  };
  
  return { getProducts, updateStock, loading, error };
}

API Routes

// src/pages/api/products/list.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { api } from '@/lib/ozon-client';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    const { limit = 20, filter } = req.query;
    
    const products = await api.product.getList({
      limit: Number(limit),
      filter: filter ? JSON.parse(filter as string) : undefined
    });
    
    res.status(200).json(products);
  } catch (error: any) {
    console.error('Products API Error:', error);
    res.status(500).json({ 
      error: 'Failed to fetch products',
      details: error.message 
    });
  }
}

React Component

// src/components/ProductManager.tsx
import React, { useState, useEffect } from 'react';
import { useProducts } from '@/hooks/useOzonAPI';

interface Product {
  id: number;
  name: string;
  offer_id: string;
  marketing_price?: string;
}

export function ProductManager() {
  const { getProducts, updateStock, loading, error } = useProducts();
  const [products, setProducts] = useState<Product[]>([]);
  
  useEffect(() => {
    loadProducts();
  }, []);
  
  const loadProducts = async () => {
    const result = await getProducts({ limit: 50 });
    if (result?.result?.items) {
      setProducts(result.result.items);
    }
  };
  
  const handleStockUpdate = async (productId: number, stock: number) => {
    const result = await updateStock([{ product_id: productId, stock }]);
    if (result) {
      // ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ локальноС состояниС
      loadProducts();
    }
  };
  
  return (
    <div className="p-6">
      <h1 className="text-2xl font-bold mb-4">Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚ΠΎΠ²Π°Ρ€Π°ΠΌΠΈ</h1>
      
      {loading && <p>Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°...</p>}
      {error && <p className="text-red-500">Ошибка: {error}</p>}
      
      <div className="grid gap-4">
        {products.map((product) => (
          <div key={product.id} className="border p-4 rounded">
            <h3 className="font-semibold">{product.name}</h3>
            <p>ID: {product.offer_id}</p>
            <p>Π¦Π΅Π½Π°: {product.marketing_price} β‚½</p>
            
            <button
              onClick={() => handleStockUpdate(product.id, 100)}
              className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
            >
              Π£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ остаток: 100
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

Express.js

Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°

src/
β”œβ”€β”€ controllers/
β”‚   β”œβ”€β”€ productController.ts
β”‚   └── orderController.ts
β”œβ”€β”€ middleware/
β”‚   β”œβ”€β”€ auth.ts
β”‚   └── errorHandler.ts
β”œβ”€β”€ routes/
β”‚   β”œβ”€β”€ products.ts
β”‚   └── orders.ts
β”œβ”€β”€ services/
β”‚   └── ozonService.ts
└── app.ts

БСрвис для OZON API

// src/services/ozonService.ts
import { OzonSellerAPI } from 'bmad-ozon-seller-api';

class OzonService {
  private api: OzonSellerAPI;
  
  constructor() {
    this.api = new OzonSellerAPI({
      clientId: process.env.OZON_CLIENT_ID!,
      apiKey: process.env.OZON_API_KEY!,
      timeout: 30000
    });
  }
  
  async getProducts(filters: any) {
    try {
      return await this.api.product.getList(filters);
    } catch (error) {
      throw new Error(`Failed to get products: ${error.message}`);
    }
  }
  
  async processOrders(status: string = 'awaiting_packaging') {
    try {
      const orders = await this.api.fbs.getOrdersList({
        filter: { status },
        limit: 50
      });
      
      const processedOrders = [];
      
      for (const order of orders.result || []) {
        // Π£ΠΏΠ°ΠΊΠΎΠ²ΠΊΠ° Π·Π°ΠΊΠ°Π·Π°
        await this.api.fbs.packOrder({
          posting_number: order.posting_number,
          packages: [{
            products: order.products.map(p => ({
              product_id: p.product_id,
              quantity: p.quantity
            }))
          }]
        });
        
        // ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° Π² доставку
        await this.api.fbs.shipOrder({
          posting_number: order.posting_number,
          tracking_number: `TRACK${Date.now()}`
        });
        
        processedOrders.push(order.posting_number);
      }
      
      return processedOrders;
    } catch (error) {
      throw new Error(`Failed to process orders: ${error.message}`);
    }
  }
  
  async updatePricesBatch(updates: Array<{product_id: number, price: string}>) {
    const batchSize = 1000;
    const results = [];
    
    for (let i = 0; i < updates.length; i += batchSize) {
      const batch = updates.slice(i, i + batchSize);
      
      try {
        const result = await this.api.pricesStocks.updatePrices(batch);
        results.push(result);
        
        // ΠŸΠ°ΡƒΠ·Π° ΠΌΠ΅ΠΆΠ΄Ρƒ Π±Π°Ρ‚Ρ‡Π°ΠΌΠΈ
        if (i + batchSize < updates.length) {
          await new Promise(resolve => setTimeout(resolve, 1000));
        }
      } catch (error) {
        console.error(`Batch ${Math.floor(i/batchSize)} failed:`, error);
        throw error;
      }
    }
    
    return results;
  }
}

export default new OzonService();

ΠšΠΎΠ½Ρ‚Ρ€ΠΎΠ»Π»Π΅Ρ€ ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚ΠΎΠ²

// src/controllers/productController.ts
import { Request, Response } from 'express';
import ozonService from '../services/ozonService';

export class ProductController {
  async getProducts(req: Request, res: Response) {
    try {
      const { limit = 20, filter } = req.query;
      
      const products = await ozonService.getProducts({
        limit: Number(limit),
        filter: filter ? JSON.parse(filter as string) : undefined
      });
      
      res.json({
        success: true,
        data: products
      });
    } catch (error: any) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  }
  
  async updatePrices(req: Request, res: Response) {
    try {
      const { updates } = req.body;
      
      if (!Array.isArray(updates)) {
        return res.status(400).json({
          success: false,
          error: 'Updates must be an array'
        });
      }
      
      const results = await ozonService.updatePricesBatch(updates);
      
      res.json({
        success: true,
        data: results,
        processed: updates.length
      });
    } catch (error: any) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  }
}

ΠœΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹

// src/routes/products.ts
import { Router } from 'express';
import { ProductController } from '../controllers/productController';

const router = Router();
const productController = new ProductController();

router.get('/list', productController.getProducts);
router.post('/prices', productController.updatePrices);

export default router;

ОсновноС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅

// src/app.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';

import productRoutes from './routes/products';
import orderRoutes from './routes/orders';
import { errorHandler } from './middleware/errorHandler';

const app = express();

// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 ΠΌΠΈΠ½ΡƒΡ‚
  max: 100 // Π»ΠΈΠΌΠΈΡ‚ Π½Π° IP
});
app.use(limiter);

// Routes
app.use('/api/products', productRoutes);
app.use('/api/orders', orderRoutes);

// Error handling
app.use(errorHandler);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

NestJS

ΠœΠΎΠ΄ΡƒΠ»ΡŒ OZON

// src/ozon/ozon.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { OzonService } from './ozon.service';
import { ProductController } from './controllers/product.controller';
import { OrderController } from './controllers/order.controller';

@Module({
  imports: [ConfigModule],
  providers: [OzonService],
  controllers: [ProductController, OrderController],
  exports: [OzonService]
})
export class OzonModule {}

БСрвис OZON

// src/ozon/ozon.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OzonSellerAPI } from 'bmad-ozon-seller-api';

@Injectable()
export class OzonService {
  private readonly logger = new Logger(OzonService.name);
  private readonly api: OzonSellerAPI;
  
  constructor(private configService: ConfigService) {
    this.api = new OzonSellerAPI({
      clientId: this.configService.get<string>('OZON_CLIENT_ID')!,
      apiKey: this.configService.get<string>('OZON_API_KEY')!,
      timeout: 30000,
      debug: this.configService.get<string>('NODE_ENV') === 'development'
    });
  }
  
  async getProducts(filters: any) {
    try {
      this.logger.log(`Fetching products with filters: ${JSON.stringify(filters)}`);
      const result = await this.api.product.getList(filters);
      this.logger.log(`Found ${result.result?.items?.length || 0} products`);
      return result;
    } catch (error) {
      this.logger.error('Failed to fetch products', error);
      throw error;
    }
  }
  
  async updateStocks(updates: Array<{product_id: number, stock: number}>) {
    try {
      this.logger.log(`Updating stocks for ${updates.length} products`);
      const result = await this.api.pricesStocks.updateStocks(updates);
      this.logger.log('Stocks updated successfully');
      return result;
    } catch (error) {
      this.logger.error('Failed to update stocks', error);
      throw error;
    }
  }
}

ΠšΠΎΠ½Ρ‚Ρ€ΠΎΠ»Π»Π΅Ρ€ с Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠ΅ΠΉ

// src/ozon/controllers/product.controller.ts
import { 
  Controller, 
  Get, 
  Post, 
  Body, 
  Query, 
  BadRequestException,
  InternalServerErrorException 
} from '@nestjs/common';
import { IsNumber, IsArray, ValidateNested, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
import { OzonService } from '../ozon.service';

class StockUpdateDto {
  @IsNumber()
  product_id: number;
  
  @IsNumber()
  stock: number;
}

class UpdateStocksDto {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => StockUpdateDto)
  updates: StockUpdateDto[];
}

@Controller('ozon/products')
export class ProductController {
  constructor(private readonly ozonService: OzonService) {}
  
  @Get('list')
  async getProducts(
    @Query('limit') limit?: string,
    @Query('filter') filter?: string
  ) {
    try {
      const filters = {
        limit: limit ? Number(limit) : 20,
        filter: filter ? JSON.parse(filter) : undefined
      };
      
      return await this.ozonService.getProducts(filters);
    } catch (error: any) {
      throw new InternalServerErrorException(error.message);
    }
  }
  
  @Post('stocks')
  async updateStocks(@Body() updateStocksDto: UpdateStocksDto) {
    try {
      return await this.ozonService.updateStocks(updateStocksDto.updates);
    } catch (error: any) {
      throw new InternalServerErrorException(error.message);
    }
  }
}

Π“Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹ΠΉ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ ошибок

// src/common/filters/ozon-exception.filter.ts
import { 
  ExceptionFilter, 
  Catch, 
  ArgumentsHost, 
  HttpException,
  Logger 
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class OzonExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(OzonExceptionFilter.name);
  
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    
    let status = 500;
    let message = 'Internal server error';
    
    if (exception instanceof HttpException) {
      status = exception.getStatus();
      message = exception.message;
    } else if (exception.message?.includes('OZON API')) {
      status = 502;
      message = 'OZON API error';
    }
    
    this.logger.error(
      `${request.method} ${request.url} - ${status} - ${message}`,
      exception.stack
    );
    
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      error: message
    });
  }
}

AWS Lambda (Serverless)

Serverless.yml конфигурация

# serverless.yml
service: ozon-api-lambda

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs18.x
  region: eu-west-1
  environment:
    OZON_CLIENT_ID: ${env:OZON_CLIENT_ID}
    OZON_API_KEY: ${env:OZON_API_KEY}
  timeout: 300
  memorySize: 512

functions:
  getProducts:
    handler: src/handlers/products.getProducts
    events:
      - httpApi:
          path: /products
          method: get
  
  processOrders:
    handler: src/handlers/orders.processOrders
    events:
      - schedule: rate(5 minutes)
      - httpApi:
          path: /orders/process
          method: post
  
  updatePrices:
    handler: src/handlers/products.updatePrices
    events:
      - httpApi:
          path: /products/prices
          method: post

plugins:
  - serverless-esbuild
  - serverless-offline

custom:
  esbuild:
    bundle: true
    minify: false
    sourcemap: true
    target: 'node18'
    define:
      'require.resolve': undefined
    platform: 'node'
    concurrency: 10

Lambda ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ

// src/handlers/products.ts
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { OzonSellerAPI } from 'bmad-ozon-seller-api';

const api = new OzonSellerAPI({
  clientId: process.env.OZON_CLIENT_ID!,
  apiKey: process.env.OZON_API_KEY!,
  timeout: 30000
});

export const getProducts = async (
  event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {
  try {
    const { limit, filter } = event.queryStringParameters || {};
    
    const products = await api.product.getList({
      limit: limit ? Number(limit) : 20,
      filter: filter ? JSON.parse(filter) : undefined
    });
    
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({
        success: true,
        data: products
      })
    };
  } catch (error: any) {
    console.error('Lambda Error:', error);
    
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        success: false,
        error: error.message
      })
    };
  }
};

export const updatePrices = async (
  event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {
  try {
    const { updates } = JSON.parse(event.body || '{}');
    
    if (!Array.isArray(updates)) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          success: false,
          error: 'Updates must be an array'
        })
      };
    }
    
    // ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π±Π°Ρ‚Ρ‡Π°ΠΌΠΈ для избСТания Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠ²
    const batchSize = 500;
    const results = [];
    
    for (let i = 0; i < updates.length; i += batchSize) {
      const batch = updates.slice(i, i + batchSize);
      const result = await api.pricesStocks.updatePrices(batch);
      results.push(result);
    }
    
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        success: true,
        data: results,
        processed: updates.length
      })
    };
  } catch (error: any) {
    return {
      statusCode: 500,
      body: JSON.stringify({
        success: false,
        error: error.message
      })
    };
  }
};

ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ Π·Π°ΠΊΠ°Π·ΠΎΠ² с ΠΏΠ»Π°Π½ΠΈΡ€ΠΎΠ²Ρ‰ΠΈΠΊΠΎΠΌ

// src/handlers/orders.ts
import { ScheduledEvent, APIGatewayProxyEventV2 } from 'aws-lambda';
import { OzonSellerAPI } from 'bmad-ozon-seller-api';

const api = new OzonSellerAPI({
  clientId: process.env.OZON_CLIENT_ID!,
  apiKey: process.env.OZON_API_KEY!
});

export const processOrders = async (
  event: ScheduledEvent | APIGatewayProxyEventV2
) => {
  try {
    console.log('Starting order processing...');
    
    // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π½ΠΎΠ²Ρ‹Π΅ Π·Π°ΠΊΠ°Π·Ρ‹
    const orders = await api.fbs.getOrdersList({
      filter: {
        status: 'awaiting_packaging'
      },
      limit: 100
    });
    
    const processedOrders = [];
    
    for (const order of orders.result || []) {
      try {
        // Π£ΠΏΠ°ΠΊΠΎΠ²ΠΊΠ° Π·Π°ΠΊΠ°Π·Π°
        await api.fbs.packOrder({
          posting_number: order.posting_number,
          packages: [{
            products: order.products.map(p => ({
              product_id: p.product_id,
              quantity: p.quantity
            }))
          }]
        });
        
        // ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° Π² доставку
        await api.fbs.shipOrder({
          posting_number: order.posting_number,
          tracking_number: `AUTO${Date.now()}`,
          shipping_provider_id: 1
        });
        
        processedOrders.push(order.posting_number);
        console.log(`Order ${order.posting_number} processed`);
        
      } catch (orderError: any) {
        console.error(`Failed to process order ${order.posting_number}:`, orderError);
      }
    }
    
    const result = {
      success: true,
      processed: processedOrders.length,
      orders: processedOrders,
      timestamp: new Date().toISOString()
    };
    
    console.log('Order processing completed:', result);
    
    // Для HTTP запросов Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ ΠΎΡ‚Π²Π΅Ρ‚
    if ('httpMethod' in event) {
      return {
        statusCode: 200,
        body: JSON.stringify(result)
      };
    }
    
    return result;
    
  } catch (error: any) {
    console.error('Order processing failed:', error);
    
    const errorResult = {
      success: false,
      error: error.message,
      timestamp: new Date().toISOString()
    };
    
    if ('httpMethod' in event) {
      return {
        statusCode: 500,
        body: JSON.stringify(errorResult)
      };
    }
    
    throw error;
  }
};

Docker контСйнСризация

Dockerfile для Production

# Dockerfile
FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Build the application
RUN npm run build

# Production image, copy all the files and run the application
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 ozonapp

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

USER ozonapp

EXPOSE 3000

ENV PORT 3000

CMD ["node", "dist/index.js"]

Docker Compose для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ

# docker-compose.yml
version: '3.8'

services:
  ozon-api:
    build:
      context: .
      target: base
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - OZON_CLIENT_ID=${OZON_CLIENT_ID}
      - OZON_API_KEY=${OZON_API_KEY}
      - REDIS_URL=redis://redis:6379
    volumes:
      - .:/app
      - /app/node_modules
    command: npm run dev
    depends_on:
      - redis
      - postgres

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ozonapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - ozon-api

volumes:
  redis_data:
  postgres_data:

ΠœΠ½ΠΎΠ³ΠΎΡΡ‚ΡƒΠΏΠ΅Π½Ρ‡Π°Ρ‚Π°Ρ сборка для микросСрвисов

# Dockerfile.microservice
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

FROM node:18-alpine AS dev
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

FROM dev AS build
RUN npm run build

FROM base AS production
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

ΠžΠ±Ρ‰ΠΈΠ΅ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ ΠΈ Π»ΡƒΡ‡ΡˆΠΈΠ΅ ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠΈ

ΠœΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ

// config/ozon.config.ts
interface OzonConfig {
  clientId: string;
  apiKey: string;
  baseURL: string;
  timeout: number;
  retryAttempts: number;
  debug: boolean;
}

export class OzonConfigManager {
  private static instance: OzonConfigManager;
  private config: OzonConfig;
  
  private constructor() {
    this.config = {
      clientId: process.env.OZON_CLIENT_ID || '',
      apiKey: process.env.OZON_API_KEY || '',
      baseURL: process.env.OZON_BASE_URL || 'https://api-seller.ozon.ru',
      timeout: Number(process.env.OZON_TIMEOUT) || 30000,
      retryAttempts: Number(process.env.OZON_RETRY_ATTEMPTS) || 3,
      debug: process.env.NODE_ENV === 'development'
    };
    
    this.validateConfig();
  }
  
  static getInstance(): OzonConfigManager {
    if (!OzonConfigManager.instance) {
      OzonConfigManager.instance = new OzonConfigManager();
    }
    return OzonConfigManager.instance;
  }
  
  private validateConfig() {
    if (!this.config.clientId) {
      throw new Error('OZON_CLIENT_ID is required');
    }
    if (!this.config.apiKey) {
      throw new Error('OZON_API_KEY is required');
    }
  }
  
  getConfig(): OzonConfig {
    return { ...this.config };
  }
}

Π£Π½ΠΈΠ²Π΅Ρ€ΡΠ°Π»ΡŒΠ½Ρ‹ΠΉ HTTP ΠΊΠ»ΠΈΠ΅Π½Ρ‚ с ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹ΠΌΠΈ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠ°ΠΌΠΈ

// utils/http-client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

export class RetryableHttpClient {
  private client: AxiosInstance;
  private maxRetries: number;
  
  constructor(config: AxiosRequestConfig, maxRetries: number = 3) {
    this.client = axios.create(config);
    this.maxRetries = maxRetries;
    
    this.setupInterceptors();
  }
  
  private setupInterceptors() {
    this.client.interceptors.response.use(
      (response) => response,
      async (error) => {
        const config = error.config;
        
        if (!config._retryCount) {
          config._retryCount = 0;
        }
        
        const shouldRetry = 
          config._retryCount < this.maxRetries &&
          (error.response?.status >= 500 || !error.response);
        
        if (shouldRetry) {
          config._retryCount++;
          
          const delay = Math.pow(2, config._retryCount) * 1000;
          await new Promise(resolve => setTimeout(resolve, delay));
          
          return this.client(config);
        }
        
        return Promise.reject(error);
      }
    );
  }
  
  async request<T>(config: AxiosRequestConfig): Promise<T> {
    const response = await this.client(config);
    return response.data;
  }
}

ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ ΠΈ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅

// utils/logger.ts
export class OzonAPILogger {
  private context: string;
  
  constructor(context: string = 'OzonAPI') {
    this.context = context;
  }
  
  log(message: string, data?: any) {
    console.log(`[${this.context}] ${new Date().toISOString()} - ${message}`, data || '');
  }
  
  error(message: string, error?: Error) {
    console.error(`[${this.context}] ${new Date().toISOString()} - ERROR: ${message}`, error || '');
  }
  
  warn(message: string, data?: any) {
    console.warn(`[${this.context}] ${new Date().toISOString()} - WARN: ${message}`, data || '');
  }
  
  performance<T>(operation: string, fn: () => Promise<T>): Promise<T> {
    const start = Date.now();
    return fn().then(
      result => {
        const duration = Date.now() - start;
        this.log(`${operation} completed in ${duration}ms`);
        return result;
      },
      error => {
        const duration = Date.now() - start;
        this.error(`${operation} failed after ${duration}ms`, error);
        throw error;
      }
    );
  }
}

Π—Π°ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅

ΠŸΡ€Π΅Π΄ΡΡ‚Π°Π²Π»Π΅Π½Π½Ρ‹Π΅ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ Π΄Π΅ΠΌΠΎΠ½ΡΡ‚Ρ€ΠΈΡ€ΡƒΡŽΡ‚ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡŽ OZON Seller API SDK с Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹ΠΌΠΈ Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ°ΠΌΠΈ ΠΈ ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ°ΠΌΠΈ. ΠšΠ°ΠΆΠ΄Ρ‹ΠΉ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚:

  • βœ… ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ окруТСния - бСзопасноС Ρ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅ ΠΊΠ»ΡŽΡ‡Π΅ΠΉ API
  • βœ… ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΡƒ ошибок - комплСксная систСма ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΉ
  • βœ… Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΠ΅ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ для ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ
  • βœ… Π’ΠΈΠΏΠΈΠ·Π°Ρ†ΠΈΡŽ - полная ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° TypeScript
  • βœ… ΠœΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΡƒΠ΅ΠΌΠΎΡΡ‚ΡŒ - Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° для роста ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°
  • βœ… Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ - Π»ΡƒΡ‡ΡˆΠΈΠ΅ ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠΈ Π·Π°Ρ‰ΠΈΡ‚Ρ‹ API

Для Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠΉ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ ΠΎΠ±Ρ€Π°Ρ‚ΠΈΡ‚Π΅ΡΡŒ ΠΊ: