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:
2025-11-16 23:57:45 +03:00
parent cefd6c98a2
commit 6bb03c03cb

View File

@@ -4,6 +4,7 @@ from django.contrib.auth.decorators import login_required
from django.http import JsonResponse from django.http import JsonResponse
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.db import transaction from django.db import transaction
from django.db.models import Prefetch, OuterRef, Subquery
from django.utils import timezone from django.utils import timezone
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
import json import json
@@ -17,60 +18,101 @@ def get_showcase_kits_for_pos():
""" """
Получает витринные комплекты для отображения в POS. Получает витринные комплекты для отображения в POS.
Возвращает список временных комплектов, которые зарезервированы на витринах. Возвращает список временных комплектов, которые зарезервированы на витринах.
Оптимизировано: убраны N+1 запросы, используется один проход по данным.
""" """
# Получаем все уникальные комплекты, у которых есть резервы на витринах from products.models import ProductKitPhoto
showcase_reservations = Reservation.objects.filter(
showcase__isnull=False,
showcase__is_active=True,
status='reserved'
).select_related('showcase', 'product').values(
'showcase_id', 'showcase__name'
).distinct()
# Собираем все kit_items, связанные с этими резервами # Получаем все зарезервированные товары на витринах
showcase_kits = []
# Получаем все временные комплекты с резервами на витринах
reserved_products = Reservation.objects.filter( reserved_products = Reservation.objects.filter(
showcase__isnull=False, showcase__isnull=False,
showcase__is_active=True, showcase__is_active=True,
status='reserved' status='reserved'
).values_list('product_id', flat=True).distinct() ).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( kits_with_showcase_items = ProductKit.objects.filter(
is_temporary=True, is_temporary=True,
status='active', status='active',
kit_items__product_id__in=reserved_products 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: for kit in kits_with_showcase_items:
# Проверяем что все компоненты этого комплекта зарезервированы на одной витрине # Используем prefetch'енные kit_items (без дополнительного запроса)
kit_product_ids = set(kit.kit_items.values_list('product_id', flat=True)) 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( all_reservations = Reservation.objects.filter(
product_id__in=kit_product_ids, product_id__in=all_kit_product_ids,
showcase__isnull=False, showcase__isnull=False,
showcase__is_active=True, showcase__is_active=True,
status='reserved' status='reserved'
).select_related('showcase') ).select_related('showcase').values('product_id', 'showcase_id', 'showcase__name')
if kit_reservations.exists(): # Группируем резервы по product_id
# Берём первую витрину (обычно комплект резервируется на одной) product_to_showcases = {} # {product_id: [(showcase_id, showcase_name), ...]}
showcase = kit_reservations.first().showcase 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({ showcase_kits.append({
'id': kit.id, 'id': kit.id,
'name': kit.name, 'name': kit.name,
'price': str(kit.actual_price), 'price': str(kit.actual_price),
'category_ids': [], # Временные комплекты обычно без категорий 'category_ids': [],
'in_stock': True, # На витрине = в наличии 'in_stock': True,
'sku': kit.sku or '', 'sku': kit.sku or '',
'image': kit.photos.first().get_thumbnail_url() if kit.photos.exists() else None, 'image': image_url,
'type': 'showcase_kit', 'type': 'showcase_kit',
'showcase_name': showcase.name, 'showcase_name': showcase_name,
'showcase_id': showcase.id 'showcase_id': showcase_id
}) })
return showcase_kits return showcase_kits
@@ -81,41 +123,75 @@ def pos_terminal(request):
""" """
Tablet-friendly POS screen prototype. Tablet-friendly POS screen prototype.
Shows categories and all items (products + kits) for quick tap-to-add. 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) 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 # Показываем все товары, не только 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] categories = [{'id': c.id, 'name': c.name} for c in categories_qs]
# Сериализация товаров # Сериализация товаров с оптимизацией фото
products = [{ products = []
'id': p.id, for p in products_qs:
'name': p.name, image_url = None
'price': str(p.actual_price), if hasattr(p, 'first_photo_list') and p.first_photo_list:
'category_ids': [c.id for c in p.categories.all()], image_url = p.first_photo_list[0].get_thumbnail_url()
'in_stock': p.in_stock,
'sku': p.sku or '',
'image': p.photos.first().get_thumbnail_url() if p.photos.exists() else None,
'type': 'product'
} for p in products_qs]
# Сериализация комплектов products.append({
kits = [{ 'id': p.id,
'id': k.id, 'name': p.name,
'name': k.name, 'price': str(p.actual_price),
'price': str(k.actual_price), 'category_ids': [c.id for c in p.categories.all()],
'category_ids': [c.id for c in k.categories.all()], 'in_stock': p.in_stock,
'in_stock': False, # Комплекты всегда "Под заказ" (пока не интегрируем проверку наличия) 'sku': p.sku or '',
'sku': k.sku or '', 'image': image_url,
'image': k.photos.first().get_thumbnail_url() if k.photos.exists() else None, 'type': 'product'
'type': 'kit' })
} for k in kits_qs]
# Получаем витринные комплекты (временные комплекты с резервами на витринах) # Сериализация комплектов с оптимизацией фото
showcase_kits_data = get_showcase_kits_for_pos() 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, # Комплекты всегда "Под заказ"
'sku': k.sku or '',
'image': image_url,
'type': 'kit'
})
# Объединяем все позиции # Объединяем все позиции
all_items = products + kits all_items = products + kits
@@ -123,7 +199,7 @@ def pos_terminal(request):
context = { context = {
'categories_json': json.dumps(categories), 'categories_json': json.dumps(categories),
'items_json': json.dumps(all_items), 'items_json': json.dumps(all_items),
'showcase_kits_json': json.dumps(showcase_kits_data), 'showcase_kits_json': json.dumps([]), # Пустой массив - загрузка по API
'title': 'POS Terminal', 'title': 'POS Terminal',
} }
return render(request, 'pos/terminal.html', context) return render(request, 'pos/terminal.html', context)