From b7c831f0c36add4c7ed3ab2ebc717f33c4bb3b1c Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 6 Apr 2026 22:11:41 +0300 Subject: [PATCH] fix(db): Fix handler for get product with categories --- api/product.py | 53 +++++---- db/handlers/product_handler.py | 198 +++++++++++++++++---------------- 2 files changed, 134 insertions(+), 117 deletions(-) diff --git a/api/product.py b/api/product.py index 80f1ea2..49277b7 100644 --- a/api/product.py +++ b/api/product.py @@ -7,47 +7,56 @@ api_product = Blueprint('api_product', __name__, url_prefix='/products') @api_product.route('/', methods=['GET'], strict_slashes=False) def get_products(): - """ - Получение товаров с фильтрацией и пагинацией - GET /api/products?categoryId=1&page=1&limit=9&color=серый&size=110х2,2х500 - """ try: - # Получаем обязательный параметр categoryId category_id = request.args.get('categoryId', type=int) if not category_id: return jsonify({"error": "categoryId is required"}), 400 - - # Получаем параметры пагинации + page = request.args.get('page', 1, type=int) limit = request.args.get('limit', 9, type=int) - - # Валидация параметров + if page < 1: page = 1 if limit < 1 or limit > 100: limit = 9 - - # Собираем фильтры из query параметров + + # Словарь для маппинга внешних ключей → внутренние имена в БД + filter_mapping = { + 'attr_1': 'diameter', + 'attr_2': 'length', + 'attr_3': 'wall_thickness', + 'attr_4': 'color', + 'attr_5': 'application_type', + 'attr_6': 'stiffness_class', + 'attr_7': 'angle', + 'attr_8': 'fitting_type', + } + filters = {} - excluded_params = ['categoryId', 'page', 'limit'] - + excluded_params = {'categoryId', 'page', 'limit'} + for key, value in request.args.items(): - if key not in excluded_params: - if ',' in value: - filters[key] = value.split(',') - else: - filters[key] = value - - # Получаем товары + if key in excluded_params: + continue + + # Преобразуем ключ, если он есть в маппинге, иначе оставляем как есть + real_key = filter_mapping.get(key, key) + + # Разделяем значения по запятой + if ',' in value: + filters[real_key] = value.split(',') + else: + filters[real_key] = value + result = get_products_with_filters( category_id=category_id, page=page, limit=limit, filters=filters if filters else None ) - + return jsonify(result), 200 - + except Exception as e: return jsonify({"error": str(e)}), 500 diff --git a/db/handlers/product_handler.py b/db/handlers/product_handler.py index 06bb4df..e6c70c7 100644 --- a/db/handlers/product_handler.py +++ b/db/handlers/product_handler.py @@ -14,122 +14,130 @@ def get_products_with_filters( cursor = conn.cursor() try: - # Базовый запрос для получения товаров - base_query = """ - SELECT DISTINCT p.id, p.name, p.full_name, p.description, - p.diameter, p.wall_thickness, p.length, p.color, - p.application_type, p.stiffness_class, p.image_path, - p.created_at - FROM products p - WHERE p.category_id = ? + # Рекурсивный CTE для получения всех подкатегорий + hierarchy_query = """ + WITH RECURSIVE category_hierarchy AS ( + SELECT id FROM categories WHERE id = ? + UNION ALL + SELECT c.id FROM categories c + JOIN category_hierarchy ch ON c.parent_id = ch.id + ) + SELECT id FROM category_hierarchy """ + cursor.execute(hierarchy_query, (category_id,)) + category_ids = [row['id'] for row in cursor.fetchall()] - params = [category_id] - - # Применяем фильтры + if not category_ids: + return {"items": [], "total": 0, "page": page, "limit": limit, "total_pages": 0} + + placeholders_cats = ','.join(['?' for _ in category_ids]) + base_where = f"WHERE p.category_id IN ({placeholders_cats})" + params = list(category_ids) + + # Обработка фильтров if filters: - for filter_key, filter_value in filters.items(): - if filter_value: - if isinstance(filter_value, list): - # Для множественных значений - placeholders = ','.join(['?' for _ in filter_value]) - base_query += f""" - AND p.id IN ( - SELECT DISTINCT pf.product_id - FROM product_filters pf - WHERE pf.filter_key = ? - AND pf.value IN ({placeholders}) - ) - """ - params.append(filter_key) - params.extend(filter_value) + column_map = { + 'diameter': 'p.diameter', + 'length': 'p.length', + 'wall_thickness': 'p.wall_thickness', + 'color': 'p.color', + 'application_type': 'p.application_type', + 'stiffness_class': 'p.stiffness_class' + } + + def cast_value(col_name, val): + if col_name in ('diameter', 'length'): + try: + return int(val) + except: + return val + if col_name == 'wall_thickness': + try: + return float(val) + except: + return val + return val + + for key, value in filters.items(): + if not value: + continue + + if key in column_map: + col = column_map[key] + if isinstance(value, list): + casted = [cast_value(key, v) for v in value] + v_ph = ','.join(['?' for _ in casted]) + base_where += f" AND {col} IN ({v_ph})" + params.extend(casted) else: - # Для одиночных значений - base_query += """ + casted = cast_value(key, value) + base_where += f" AND {col} = ?" + params.append(casted) + else: + # Фильтры из product_filters + if isinstance(value, list): + v_ph = ','.join(['?' for _ in value]) + base_where += f""" AND p.id IN ( - SELECT DISTINCT pf.product_id - FROM product_filters pf - WHERE pf.filter_key = ? AND pf.value = ? - ) - """ - params.append(filter_key) - params.append(filter_value) - - # Получаем общее количество товаров - count_query = f"SELECT COUNT(*) as total FROM ({base_query})" + SELECT product_id FROM product_filters + WHERE filter_key = ? AND value IN ({v_ph}) + )""" + params.append(key) + params.extend([str(v) for v in value]) + else: + base_where += f""" + AND p.id IN ( + SELECT product_id FROM product_filters + WHERE filter_key = ? AND value = ? + )""" + params.append(key) + params.append(str(value)) + + # Подсчёт общего количества + count_query = f"SELECT COUNT(*) as total FROM products p {base_where}" cursor.execute(count_query, params) total = cursor.fetchone()['total'] - - # Добавляем пагинацию + + # Пагинация offset = (page - 1) * limit - paginated_query = f""" - {base_query} - ORDER BY p.id + query = f""" + SELECT p.* FROM products p + {base_where} + ORDER BY p.id ASC LIMIT ? OFFSET ? """ - params.extend([limit, offset]) - - cursor.execute(paginated_query, params) - - # Формируем результат + cursor.execute(query, params + [limit, offset]) + rows = cursor.fetchall() + items = [] - for row in cursor.fetchall(): - # Формируем атрибуты товара - attributes = {} - - if row['color']: - attributes['color'] = row['color'] - if row['length'] and row['length'] > 0: - if row['diameter'] and row['wall_thickness']: - attributes['size'] = f"{row['diameter']}x{row['wall_thickness']}x{row['length']}" - else: - attributes['size'] = str(row['length']) - if row['diameter']: - attributes['diameter'] = str(row['diameter']) - if row['wall_thickness']: - attributes['wall_thickness'] = str(row['wall_thickness']) - if row['stiffness_class']: - attributes['stiffness_class'] = row['stiffness_class'] - if row['application_type']: - attributes['application_type'] = row['application_type'] - - # Получаем дополнительные атрибуты из product_filters - cursor.execute(""" - SELECT filter_key, value - FROM product_filters - WHERE product_id = ? - """, (row['id'],)) - - for filter_row in cursor.fetchall(): - if filter_row['filter_key'] not in attributes: - attributes[filter_row['filter_key']] = filter_row['value'] - - # Определяем количество в упаковке (можно вынести в отдельную таблицу) - attributes['amount'] = get_pack_amount(row['id'], row['diameter'], conn) - + for row in rows: items.append({ "id": row['id'], "name": row['name'], - "description": row['description'] or f"Труба ПВХ {row['diameter']}мм" if row['diameter'] else "Товар", - # Цена не хранится в БД - возвращаем 0 или None - "price": None, - "categoryId": category_id, - "images": get_product_images(row['id'], row['image_path'], conn), - "attributes": attributes, - "inStock": check_stock(row['id'], conn) + "full_name": row['full_name'], + "description": row['description'], + "categoryId": row['category_id'], + "images": [row['image_path']] if row['image_path'] else ["/images/products/default.jpg"], + "attributes": { + "diameter": row['diameter'], + "length": row['length'], + "wall_thickness": row['wall_thickness'], + "color": row['color'], + "application_type": row['application_type'] + } }) - + return { "items": items, "total": total, "page": page, "limit": limit, - "total_pages": (total + limit - 1) // limit + "total_pages": (total + limit - 1) // limit if total > 0 else 0 } - - except sqlite3.Error as e: - log.error(f"Ошибка при получении товаров: {e}") - return {"items": [], "total": 0, "page": page, "limit": limit} + + except Exception as e: + log.error(f"SQL Error in get_products_with_filters: {e}") + return {"items": [], "total": 0, "page": page, "limit": limit, "total_pages": 0} finally: conn.close()