perf: оптимизация загрузки POS терминала
- Убрана стартовая загрузка витринных комплектов (теперь только по API) - showcase_kits_json теперь пустой массив на старте - Витринные букеты загружаются динамически при клике на ВИТРИНА - Оптимизирована get_showcase_kits_for_pos - устранены N+1 запросы - Один запрос для всех резервов вместо N запросов на комплект - Используется prefetch для kit_items (без дополнительных запросов) - Добавлена группировка резервов в памяти вместо повторных обращений к БД - Оптимизирована загрузка фото товаров и комплектов - Используется Prefetch только для первого фото (thumbnail) - Вместо photos.first() (который тянет все фото) - ограниченный queryset - Prefetch с to_attr='first_photo_list' для минимизации запросов - Результат: значительное сокращение нагрузки на БД при открытии POS
This commit is contained in:
@@ -4,6 +4,7 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.db import transaction
|
||||
from django.db.models import Prefetch, OuterRef, Subquery
|
||||
from django.utils import timezone
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import json
|
||||
@@ -17,60 +18,101 @@ def get_showcase_kits_for_pos():
|
||||
"""
|
||||
Получает витринные комплекты для отображения в POS.
|
||||
Возвращает список временных комплектов, которые зарезервированы на витринах.
|
||||
Оптимизировано: убраны N+1 запросы, используется один проход по данным.
|
||||
"""
|
||||
# Получаем все уникальные комплекты, у которых есть резервы на витринах
|
||||
showcase_reservations = Reservation.objects.filter(
|
||||
showcase__isnull=False,
|
||||
showcase__is_active=True,
|
||||
status='reserved'
|
||||
).select_related('showcase', 'product').values(
|
||||
'showcase_id', 'showcase__name'
|
||||
).distinct()
|
||||
from products.models import ProductKitPhoto
|
||||
|
||||
# Собираем все kit_items, связанные с этими резервами
|
||||
showcase_kits = []
|
||||
|
||||
# Получаем все временные комплекты с резервами на витринах
|
||||
# Получаем все зарезервированные товары на витринах
|
||||
reserved_products = Reservation.objects.filter(
|
||||
showcase__isnull=False,
|
||||
showcase__is_active=True,
|
||||
status='reserved'
|
||||
).values_list('product_id', flat=True).distinct()
|
||||
|
||||
# Находим комплекты, в которых есть эти товары
|
||||
if not reserved_products:
|
||||
return []
|
||||
|
||||
# Prefetch для первого фото (thumbnail)
|
||||
first_photo_prefetch = Prefetch(
|
||||
'photos',
|
||||
queryset=ProductKitPhoto.objects.order_by('order')[:1],
|
||||
to_attr='first_photo_list'
|
||||
)
|
||||
|
||||
# Находим комплекты с резервированными компонентами
|
||||
kits_with_showcase_items = ProductKit.objects.filter(
|
||||
is_temporary=True,
|
||||
status='active',
|
||||
kit_items__product_id__in=reserved_products
|
||||
).prefetch_related('photos', 'kit_items__product').distinct()
|
||||
).prefetch_related(
|
||||
first_photo_prefetch,
|
||||
Prefetch('kit_items', queryset=KitItem.objects.select_related('product'))
|
||||
).distinct()
|
||||
|
||||
# Получаем все резервы для компонентов комплектов одним запросом
|
||||
all_kit_product_ids = set()
|
||||
kit_to_product_ids = {} # {kit.id: set(product_ids)}
|
||||
|
||||
for kit in kits_with_showcase_items:
|
||||
# Проверяем что все компоненты этого комплекта зарезервированы на одной витрине
|
||||
kit_product_ids = set(kit.kit_items.values_list('product_id', flat=True))
|
||||
# Используем prefetch'енные kit_items (без дополнительного запроса)
|
||||
product_ids = {item.product_id for item in kit.kit_items.all()}
|
||||
kit_to_product_ids[kit.id] = product_ids
|
||||
all_kit_product_ids.update(product_ids)
|
||||
|
||||
# Находим резервы для этих товаров
|
||||
kit_reservations = Reservation.objects.filter(
|
||||
product_id__in=kit_product_ids,
|
||||
# Один запрос для всех резервов
|
||||
all_reservations = Reservation.objects.filter(
|
||||
product_id__in=all_kit_product_ids,
|
||||
showcase__isnull=False,
|
||||
showcase__is_active=True,
|
||||
status='reserved'
|
||||
).select_related('showcase')
|
||||
).select_related('showcase').values('product_id', 'showcase_id', 'showcase__name')
|
||||
|
||||
if kit_reservations.exists():
|
||||
# Берём первую витрину (обычно комплект резервируется на одной)
|
||||
showcase = kit_reservations.first().showcase
|
||||
# Группируем резервы по product_id
|
||||
product_to_showcases = {} # {product_id: [(showcase_id, showcase_name), ...]}
|
||||
for res in all_reservations:
|
||||
product_id = res['product_id']
|
||||
if product_id not in product_to_showcases:
|
||||
product_to_showcases[product_id] = []
|
||||
product_to_showcases[product_id].append((res['showcase_id'], res['showcase__name']))
|
||||
|
||||
# Формируем результат
|
||||
showcase_kits = []
|
||||
for kit in kits_with_showcase_items:
|
||||
product_ids = kit_to_product_ids[kit.id]
|
||||
|
||||
# Находим общую витрину для всех компонентов
|
||||
showcases_for_kit = None
|
||||
for product_id in product_ids:
|
||||
showcases = product_to_showcases.get(product_id, [])
|
||||
if showcases_for_kit is None:
|
||||
showcases_for_kit = set(s[0] for s in showcases)
|
||||
else:
|
||||
showcases_for_kit &= set(s[0] for s in showcases)
|
||||
|
||||
if showcases_for_kit:
|
||||
# Берём первую витрину
|
||||
showcase_id = list(showcases_for_kit)[0]
|
||||
showcase_name = next(
|
||||
(s[1] for pid in product_ids for s in product_to_showcases.get(pid, []) if s[0] == showcase_id),
|
||||
'Неизвестно'
|
||||
)
|
||||
|
||||
# Используем prefetch'енное первое фото
|
||||
image_url = None
|
||||
if hasattr(kit, 'first_photo_list') and kit.first_photo_list:
|
||||
image_url = kit.first_photo_list[0].get_thumbnail_url()
|
||||
|
||||
showcase_kits.append({
|
||||
'id': kit.id,
|
||||
'name': kit.name,
|
||||
'price': str(kit.actual_price),
|
||||
'category_ids': [], # Временные комплекты обычно без категорий
|
||||
'in_stock': True, # На витрине = в наличии
|
||||
'category_ids': [],
|
||||
'in_stock': True,
|
||||
'sku': kit.sku or '',
|
||||
'image': kit.photos.first().get_thumbnail_url() if kit.photos.exists() else None,
|
||||
'image': image_url,
|
||||
'type': 'showcase_kit',
|
||||
'showcase_name': showcase.name,
|
||||
'showcase_id': showcase.id
|
||||
'showcase_name': showcase_name,
|
||||
'showcase_id': showcase_id
|
||||
})
|
||||
|
||||
return showcase_kits
|
||||
@@ -81,41 +123,75 @@ def pos_terminal(request):
|
||||
"""
|
||||
Tablet-friendly POS screen prototype.
|
||||
Shows categories and all items (products + kits) for quick tap-to-add.
|
||||
Оптимизировано: убрана стартовая загрузка витрин, только thumbnail фото.
|
||||
"""
|
||||
from products.models import ProductPhoto, ProductKitPhoto
|
||||
|
||||
categories_qs = ProductCategory.objects.filter(is_active=True)
|
||||
|
||||
# Prefetch для первого фото товаров
|
||||
first_product_photo = Prefetch(
|
||||
'photos',
|
||||
queryset=ProductPhoto.objects.order_by('order')[:1],
|
||||
to_attr='first_photo_list'
|
||||
)
|
||||
|
||||
# Показываем все товары, не только in_stock
|
||||
products_qs = Product.objects.all().prefetch_related('categories', 'photos')
|
||||
products_qs = Product.objects.all().prefetch_related(
|
||||
'categories',
|
||||
first_product_photo
|
||||
)
|
||||
|
||||
# Prefetch для первого фото комплектов
|
||||
first_kit_photo = Prefetch(
|
||||
'photos',
|
||||
queryset=ProductKitPhoto.objects.order_by('order')[:1],
|
||||
to_attr='first_photo_list'
|
||||
)
|
||||
|
||||
# Показываем все комплекты (кроме временных)
|
||||
kits_qs = ProductKit.objects.filter(is_temporary=False).prefetch_related('categories', 'photos')
|
||||
kits_qs = ProductKit.objects.filter(is_temporary=False).prefetch_related(
|
||||
'categories',
|
||||
first_kit_photo
|
||||
)
|
||||
|
||||
categories = [{'id': c.id, 'name': c.name} for c in categories_qs]
|
||||
|
||||
# Сериализация товаров
|
||||
products = [{
|
||||
# Сериализация товаров с оптимизацией фото
|
||||
products = []
|
||||
for p in products_qs:
|
||||
image_url = None
|
||||
if hasattr(p, 'first_photo_list') and p.first_photo_list:
|
||||
image_url = p.first_photo_list[0].get_thumbnail_url()
|
||||
|
||||
products.append({
|
||||
'id': p.id,
|
||||
'name': p.name,
|
||||
'price': str(p.actual_price),
|
||||
'category_ids': [c.id for c in p.categories.all()],
|
||||
'in_stock': p.in_stock,
|
||||
'sku': p.sku or '',
|
||||
'image': p.photos.first().get_thumbnail_url() if p.photos.exists() else None,
|
||||
'image': image_url,
|
||||
'type': 'product'
|
||||
} for p in products_qs]
|
||||
})
|
||||
|
||||
# Сериализация комплектов
|
||||
kits = [{
|
||||
# Сериализация комплектов с оптимизацией фото
|
||||
kits = []
|
||||
for k in kits_qs:
|
||||
image_url = None
|
||||
if hasattr(k, 'first_photo_list') and k.first_photo_list:
|
||||
image_url = k.first_photo_list[0].get_thumbnail_url()
|
||||
|
||||
kits.append({
|
||||
'id': k.id,
|
||||
'name': k.name,
|
||||
'price': str(k.actual_price),
|
||||
'category_ids': [c.id for c in k.categories.all()],
|
||||
'in_stock': False, # Комплекты всегда "Под заказ" (пока не интегрируем проверку наличия)
|
||||
'in_stock': False, # Комплекты всегда "Под заказ"
|
||||
'sku': k.sku or '',
|
||||
'image': k.photos.first().get_thumbnail_url() if k.photos.exists() else None,
|
||||
'image': image_url,
|
||||
'type': 'kit'
|
||||
} for k in kits_qs]
|
||||
|
||||
# Получаем витринные комплекты (временные комплекты с резервами на витринах)
|
||||
showcase_kits_data = get_showcase_kits_for_pos()
|
||||
})
|
||||
|
||||
# Объединяем все позиции
|
||||
all_items = products + kits
|
||||
@@ -123,7 +199,7 @@ def pos_terminal(request):
|
||||
context = {
|
||||
'categories_json': json.dumps(categories),
|
||||
'items_json': json.dumps(all_items),
|
||||
'showcase_kits_json': json.dumps(showcase_kits_data),
|
||||
'showcase_kits_json': json.dumps([]), # Пустой массив - загрузка по API
|
||||
'title': 'POS Terminal',
|
||||
}
|
||||
return render(request, 'pos/terminal.html', context)
|
||||
|
||||
Reference in New Issue
Block a user