From 6bb03c03cbd727f2efaa99a10e2cd575d54ddb04 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 16 Nov 2025 23:57:45 +0300 Subject: [PATCH] =?UTF-8?q?perf:=20=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B8=20POS=20=D1=82=D0=B5=D1=80=D0=BC=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Убрана стартовая загрузка витринных комплектов (теперь только по 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 --- myproject/pos/views.py | 194 ++++++++++++++++++++++++++++------------- 1 file changed, 135 insertions(+), 59 deletions(-) diff --git a/myproject/pos/views.py b/myproject/pos/views.py index 5bb15d8..1f951d0 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -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) + + # Один запрос для всех резервов + all_reservations = Reservation.objects.filter( + product_id__in=all_kit_product_ids, + showcase__isnull=False, + showcase__is_active=True, + status='reserved' + ).select_related('showcase').values('product_id', 'showcase_id', 'showcase__name') + + # Группируем резервы по 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] - # Находим резервы для этих товаров - kit_reservations = Reservation.objects.filter( - product_id__in=kit_product_ids, - showcase__isnull=False, - showcase__is_active=True, - status='reserved' - ).select_related('showcase') + # Находим общую витрину для всех компонентов + 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 kit_reservations.exists(): - # Берём первую витрину (обычно комплект резервируется на одной) - showcase = kit_reservations.first().showcase + 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 = [{ - '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, - 'type': 'product' - } for p in products_qs] + # Сериализация товаров с оптимизацией фото + 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': image_url, + 'type': 'product' + }) - # Сериализация комплектов - kits = [{ - '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': k.photos.first().get_thumbnail_url() if k.photos.exists() else None, - '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 @@ -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)