Разработка интернет-магазина на Saleor
Saleor — Python/Django e-commerce платформа с GraphQL API как единственным интерфейсом. Стек: Django 4.x + Graphene-Django, PostgreSQL, Celery + Redis для фоновых задач, OpenTelemetry для трейсинга. Архитектурно Saleor — headless по умолчанию: бэкенд предоставляет GraphQL API, фронтенд строится отдельно (официальный starter на Next.js — saleor/storefront).
Архитектура Saleor
┌─────────────────────────────────────┐
│ Saleor Core (Django) │
├──────────────┬──────────────────────┤
│ GraphQL API │ REST Webhooks │
│ (Graphene) │ (Events) │
├──────────────┴──────────────────────┤
│ Channel System (мультирегион) │
├──────────┬────────────┬─────────────┤
│ Products │ Checkout │ Orders │
│ + Attrs │ + Payments│ + Shipping │
├──────────┴────────────┴─────────────┤
│ PostgreSQL │ Redis │ Celery │
└─────────────────────────────────────┘
Ключевая концепция Saleor — Channel (канал): каждый канал имеет собственную валюту, страны доставки, ценообразование, правила складского учёта. Один продукт может быть доступен в нескольких каналах с разными ценами.
Установка и начальная конфигурация
# Клонирование и настройка
git clone https://github.com/saleor/saleor.git
cd saleor
# Через Docker Compose (рекомендуется для разработки)
docker compose up -d
# Ручная установка
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Переменные окружения (.env)
cat > .env << 'EOF'
SECRET_KEY=your-django-secret-key-50-chars-min
DATABASE_URL=postgresql://saleor:saleorpass@localhost:5432/saleor
CELERY_BROKER_URL=redis://localhost:6379/0
[email protected]
ALLOWED_HOSTS=localhost,api.example.com
ALLOWED_CLIENT_HOSTS=localhost:3000,store.example.com
AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=xxx
AWS_STORAGE_BUCKET_NAME=saleor-media
DEFAULT_CHANNEL_SLUG=default-channel
EOF
# Миграции и начальные данные
python manage.py migrate
python manage.py collectstatic --no-input
python manage.py createsuperuser
python manage.py populatedb --createsuperuser # seed демо-данных
GraphQL API: структура запросов
Весь фронтенд взаимодействует с Saleor через GraphQL. Endpoint: /graphql/.
Получение списка продуктов для канала:
query ProductList($channel: String!, $first: Int!, $after: String) {
products(channel: $channel, first: $first, after: $after) {
edges {
node {
id
name
slug
description
thumbnail(size: 800, format: WEBP) { url alt }
pricing {
priceRange {
start { gross { amount currency } }
stop { gross { amount currency } }
}
onSale
discount { gross { amount } }
}
variants {
id
name
sku
quantityAvailable(countryCode: RU)
pricing {
price { gross { amount currency } }
}
attributes {
attribute { name slug }
values { name slug }
}
}
category { name slug }
collections { name slug }
}
}
pageInfo { hasNextPage endCursor }
totalCount
}
}
Channel System — мультирегиональность
Channel — ключевой механизм для работы с несколькими рынками:
# Создание канала для российского рынка (Admin GraphQL)
mutation CreateChannel {
channelCreate(input: {
name: "Россия"
slug: "ru"
currencyCode: "RUB"
defaultCountry: RU
countries: [RU, BY, KZ]
stockSettings: {
allocationStrategy: PRIORITIZE_HIGH_STOCK
}
orderSettings: {
automaticallyConfirmAllNewOrders: false
automaticallyFulfillNonShippableGiftCard: true
}
checkoutSettings: {
useLegacyErrorFlow: false
}
}) {
channel {
id slug name currencyCode
}
errors { field message code }
}
}
Привязка продукта к каналу с ценообразованием:
# saleor/product/models.py — программно через Django ORM
from saleor.product.models import Product, ProductChannelListing
from saleor.channel.models import Channel
channel = Channel.objects.get(slug='ru')
product = Product.objects.get(slug='kurtka-zimnyaya')
listing = ProductChannelListing.objects.create(
product=product,
channel=channel,
is_published=True,
visible_in_listings=True,
available_for_purchase_at=timezone.now(),
)
# Цены на варианты
from saleor.product.models import ProductVariant, ProductVariantChannelListing
from saleor.warehouse.models import Warehouse, Stock
variant = ProductVariant.objects.get(sku='JACKET-S-BLACK')
ProductVariantChannelListing.objects.create(
variant=variant,
channel=channel,
price_amount=Decimal('2999.00'),
currency=channel.currency_code,
)
Кастомные атрибуты и типы продуктов
Saleor использует Product Type → Attribute систему:
# Создание кастомного атрибута
from saleor.attribute.models import Attribute, AttributeValue
material_attr = Attribute.objects.create(
name='Материал',
slug='material',
type=AttributeType.PRODUCT_TYPE,
input_type=AttributeInputType.DROPDOWN,
filterable_in_storefront=True,
filterable_in_dashboard=True,
available_in_grid=True,
)
for material in ['Хлопок', 'Полиэстер', 'Шерсть', 'Нейлон']:
AttributeValue.objects.create(
attribute=material_attr,
name=material,
slug=slugify(material, allow_unicode=True),
)
# Привязка к Product Type
from saleor.product.models import ProductType
product_type = ProductType.objects.get(slug='clothing')
product_type.product_attributes.add(material_attr)
Webhooks и интеграции
Saleor отправляет события через webhooks. Список ключевых событий:
| Событие | Триггер |
|---|---|
ORDER_CREATED |
Создание заказа |
ORDER_PAID |
Оплата заказа |
ORDER_FULFILLED |
Отгрузка |
PRODUCT_UPDATED |
Изменение продукта |
CHECKOUT_FILTER_SHIPPING_METHODS |
Синхронная фильтрация методов доставки |
PAYMENT_GATEWAY_INITIALIZE_SESSION |
Инициализация платёжной сессии |
Регистрация webhook через Admin API:
mutation CreateWebhook {
webhookCreate(input: {
name: "CRM Order Sync"
targetUrl: "https://crm.example.com/saleor/orders"
events: [ORDER_CREATED, ORDER_PAID, ORDER_CANCELLED]
secretKey: "webhook-secret-key-here"
isActive: true
asyncEvents: [ORDER_CREATED, ORDER_PAID]
syncEvents: []
}) {
webhook { id name targetUrl }
errors { field message }
}
}
Обработка webhook:
# В Django view (или FastAPI/любой другой сервис)
import hashlib, hmac
from django.http import JsonResponse
def saleor_webhook(request):
# Верификация подписи
signature = request.headers.get('Saleor-Signature', '')
secret = b'webhook-secret-key-here'
computed = hmac.new(secret, request.body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, computed):
return JsonResponse({'error': 'Invalid signature'}, status=400)
event_type = request.headers.get('Saleor-Event')
payload = json.loads(request.body)
if event_type == 'ORDER_PAID':
order_id = payload['order']['id']
sync_order_to_crm.delay(order_id) # Celery task
return JsonResponse({'status': 'ok'})
Apps и платёжные провайдеры
Saleor использует концепцию Saleor Apps — отдельные микросервисы, подключаемые через App Store или самостоятельно:
# Кастомный платёжный провайдер через Synchronous Webhooks
# При PAYMENT_GATEWAY_INITIALIZE_SESSION Saleor ожидает синхронный ответ
@app.post("/saleor/payment-gateway-initialize")
async def payment_initialize(request: Request):
payload = await request.json()
return {
"data": {
"publishableKey": settings.CLOUDPAYMENTS_PUBLIC_ID,
"currency": payload["sourceObject"]["currency"],
}
}
@app.post("/saleor/transaction-initialize")
async def transaction_initialize(request: Request):
payload = await request.json()
amount = payload["action"]["amount"]
currency = payload["action"]["currency"]
# Создание транзакции в CloudPayments
transaction = await cloudpayments_client.create_order(amount, currency)
return {
"pspReference": transaction["id"],
"result": "CHARGE_ACTION_REQUIRED",
"data": {"confirmationToken": transaction["token"]},
"amount": amount,
"currency": currency,
"externalUrl": transaction["url"],
}
Dashboard (Admin) — Next.js приложение
Официальный Saleor Dashboard — отдельное React/Next.js приложение:
git clone https://github.com/saleor/saleor-dashboard.git
cd saleor-dashboard
cat > .env << 'EOF'
NEXT_PUBLIC_API_URI=https://api.example.com/graphql/
NEXT_PUBLIC_SALEOR_APPS_PAGE_PATH=/extensions
EOF
npm install
npm run dev # http://localhost:9000
npm run build # production build
Celery для фоновых задач
# saleor/celeryconf.py уже настроен, добавление кастомных задач:
from celery import shared_task
from django.utils import timezone
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def sync_inventory_from_erp(self, product_variant_id: str):
try:
from saleor.warehouse.models import Stock
from .erp_client import ErpClient
client = ErpClient()
sku = ProductVariant.objects.get(id=product_variant_id).sku
quantity = client.get_stock(sku)
Stock.objects.filter(
product_variant_id=product_variant_id
).update(quantity=quantity, quantity_allocated=models.F('quantity_allocated'))
except Exception as exc:
raise self.retry(exc=exc)
Производительность и масштабирование
Saleor тяжёл на GraphQL-запросах с глубокими связями. Ключевые настройки:
# settings.py — настройки для production
GRAPHQL_MIDDLEWARE = [
'saleor.graphql.middleware.OpMetricsMiddleware',
]
# DataLoader включён по умолчанию через Promise/graphene
# Настройка кэша Django для фрагментов
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": os.environ.get("REDIS_URL"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100},
},
"TIMEOUT": 300,
}
}
# Query complexity limit
GRAPHQL_QUERY_MAX_COMPLEXITY = 50000
# Nginx: кэширование статических ответов GraphQL (только для публичных)
location /graphql/ {
proxy_cache_valid 200 1m;
proxy_cache_key "$request_method$request_uri$request_body";
proxy_cache_bypass $cookie_session_id; # не кэшировать авторизованные
}
Sроки разработки
- Базовая настройка + Saleor Dashboard + Next.js Storefront Starter: 2–3 недели
- Магазин с кастомными типами продуктов, атрибутами, каналами (2–3 рынка): 5–8 недель
- Полная e-commerce платформа: кастомные App, интеграции ERP/CRM, кастомные платёжные провайдеры, мультиканальность: 14–20 недель







