From 098a6aa38a1a8fd3c3326f5281becab2198d871b Mon Sep 17 00:00:00 2001 From: Kirill Date: Sun, 26 Jul 2026 20:13:33 +0300 Subject: [PATCH] fix(db): Added auto generating slug for products --- db/DBDataUtils.py | 27 ++++++++++-- db/InitializeDB.py | 43 +++++++++++++++++- db/InitializeDBData.py | 62 +++++++++++++------------- db/handlers/product_handler.py | 81 +++++++++++++++++++++++++++++++++- 4 files changed, 176 insertions(+), 37 deletions(-) diff --git a/db/DBDataUtils.py b/db/DBDataUtils.py index efb2254..a4b6a68 100644 --- a/db/DBDataUtils.py +++ b/db/DBDataUtils.py @@ -1,6 +1,7 @@ import re import sqlite3 from loguru import logger as log +from utils.slugify import slugify def add_category(conn, name, description=None, parent_id=None, image_path=None): cursor = conn.cursor() @@ -76,7 +77,7 @@ def parse_product_data(product_text): def add_product(conn, product_text, category_id, image_path=None, diameter=None, wall_thickness=None, length=None, color=None, application_type=None, stiffness_class=None, - fitting_type=None, angle=None): + fitting_type=None, angle=None, slug=None, artikul=None): """ Добавление товара с возможностью явного указания параметров Если параметры не указаны, они парсятся из product_text @@ -113,13 +114,30 @@ def add_product(conn, product_text, category_id, image_path=None, description = ", ".join(description_parts) + if fitting_type: + slug_source = product_text + else: + slug_parts = [product_text] + if stiffness_class: + slug_parts.append(stiffness_class) + if diameter: + slug_parts.append(f"d{diameter}") + if wall_thickness: + slug_parts.append(f"stenka{int(wall_thickness)}") + if length and length > 0: + slug_parts.append(f"l{length}") + slug_source = " ".join(slug_parts) + slug_value = slug or slugify(slug_source) + if artikul: + slug_value = f"{slug_value}-{artikul.lower()}" + try: cursor.execute(''' INSERT INTO products ( name, description, category_id, diameter, wall_thickness, length, color, - application_type, stiffness_class, image_path - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + application_type, stiffness_class, image_path, slug + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( product_text, description, @@ -130,7 +148,8 @@ def add_product(conn, product_text, category_id, image_path=None, color, application_type, stiffness_class, - image_path + image_path, + slug_value )) product_id = cursor.lastrowid diff --git a/db/InitializeDB.py b/db/InitializeDB.py index 847d200..f5f9cb1 100644 --- a/db/InitializeDB.py +++ b/db/InitializeDB.py @@ -1,5 +1,6 @@ import sqlite3 from loguru import logger as log +from utils.slugify import slugify def init_database(db_name='pipe_store.db'): @@ -31,6 +32,7 @@ def init_database(db_name='pipe_store.db'): application_type TEXT, -- внутренняя/наружная stiffness_class TEXT, -- SN4, SN2 и т.д. image_path TEXT, + slug TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES categories (id) ) @@ -62,6 +64,13 @@ def init_database(db_name='pipe_store.db'): cursor.execute('CREATE INDEX IF NOT EXISTS idx_products_length ON products(length)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_product_filters_key_value ON product_filters(filter_key, value)') + # Миграция: добавляем колонку slug, если её нет + cursor.execute("PRAGMA table_info(products)") + columns = {row[1] for row in cursor.fetchall()} + if 'slug' not in columns: + cursor.execute("ALTER TABLE products ADD COLUMN slug TEXT") + log.info("Migration: added 'slug' column to products") + # Миграция: переименовываем full_name → name cursor.execute("PRAGMA table_info(products)") columns = {row[1] for row in cursor.fetchall()} @@ -80,11 +89,12 @@ def init_database(db_name='pipe_store.db'): application_type TEXT, stiffness_class TEXT, image_path TEXT, + slug TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES categories (id) ); - INSERT INTO products_new (id, name, description, category_id, diameter, wall_thickness, length, color, application_type, stiffness_class, image_path, created_at) - SELECT id, full_name, description, category_id, diameter, wall_thickness, length, color, application_type, stiffness_class, image_path, created_at FROM products; + INSERT INTO products_new (id, name, description, category_id, diameter, wall_thickness, length, color, application_type, stiffness_class, image_path, slug, created_at) + SELECT id, full_name, description, category_id, diameter, wall_thickness, length, color, application_type, stiffness_class, image_path, slug, created_at FROM products; DROP TABLE products; ALTER TABLE products_new RENAME TO products; CREATE INDEX IF NOT EXISTS idx_products_category ON products(category_id); @@ -93,6 +103,35 @@ def init_database(db_name='pipe_store.db'): ''') log.info("Migrated products table: renamed 'full_name' → 'name'") + # Миграция: заполняем slug для старых товаров + cursor.execute(""" + SELECT p.id, p.name, p.diameter, p.wall_thickness, p.length, + p.stiffness_class, p.application_type, + pf.value as fitting_type + FROM products p + LEFT JOIN product_filters pf ON pf.product_id = p.id AND pf.filter_key = 'fitting_type' + WHERE p.slug IS NULL OR p.slug = '' + """) + rows = cursor.fetchall() + for row in rows: + id_, name, diameter, wall_thickness, length, stiffness_class, application_type, fitting_type = row + if fitting_type: + slug_source = name + else: + slug_parts = [name] + if stiffness_class: + slug_parts.append(stiffness_class) + if diameter: + slug_parts.append(f"d{diameter}") + if wall_thickness: + slug_parts.append(f"stenka{int(wall_thickness)}") + if length and length > 0: + slug_parts.append(f"l{length}") + slug_source = " ".join(slug_parts) + slug = slugify(slug_source) + cursor.execute("UPDATE products SET slug = ? WHERE id = ?", (slug, id_)) + log.info(f"Backfill slug for product {id_}: {slug}") + conn.commit() conn.close() log.success("База данных успешно инициализирована!") diff --git a/db/InitializeDBData.py b/db/InitializeDBData.py index 09d60ea..ae3b092 100644 --- a/db/InitializeDBData.py +++ b/db/InitializeDBData.py @@ -108,21 +108,22 @@ def init_tables(db_name='pipe_store.db'): # 1. Внутренние трубы internal_pipes = [ - ("110х2,2х500", 110, 2.2, 500, "серый", "внутренняя", None), - ("110х2,2х1000", 110, 2.2, 1000, "серый", "внутренняя", None), - ("110х2,2х1500", 110, 2.2, 1500, "серый", "внутренняя", None), - ("110х2,2х2000", 110, 2.2, 2000, "серый", "внутренняя", None), - ("110х2,2х3000", 110, 2.2, 3000, "серый", "внутренняя", None), + ("110х2,2х500", 110, 2.2, 500, "серый", "внутренняя", None, "RK011022500"), + ("110х2,2х1000", 110, 2.2, 1000, "серый", "внутренняя", None, "RK0110221000"), + ("110х2,2х1500", 110, 2.2, 1500, "серый", "внутренняя", None, "RK0110221500"), + ("110х2,2х2000", 110, 2.2, 2000, "серый", "внутренняя", None, "RK0110222000"), + ("110х2,2х3000", 110, 2.2, 3000, "серый", "внутренняя", None, "RK0110223000"), ] - for size, diameter, thickness, length, color, app_type, stiffness in internal_pipes: + for size, diameter, thickness, length, color, app_type, stiffness, artikul in internal_pipes: full_name = f"Труба НПВХ {diameter}x{thickness}x{length} мм внутренняя" # Замена на внешнюю ссылку image_url = f"{base_url}/products/internal_{diameter}_{length}.png" product_id = add_product( conn, full_name, internal_category_id, image_url, - diameter, thickness, length, color, app_type, stiffness + diameter, thickness, length, color, app_type, stiffness, + artikul=artikul ) if product_id: added_products += 1 @@ -130,30 +131,30 @@ def init_tables(db_name='pipe_store.db'): # 2. Наружные трубы external_pipes = [ - ("110х2,3х500", 110, 2.3, 500, "оранжево-коричневый", "наружная", None), - ("110х2,3х1000", 110, 2.3, 1000, "оранжево-коричневый", "наружная", None), - ("110х2,3х2000", 110, 2.3, 2000, "оранжево-коричневый", "наружная", None), - ("110х2,3х3000", 110, 2.3, 3000, "оранжево-коричневый", "наружная", None), - ("110х2,3х4000", 110, 2.3, 4000, "оранжево-коричневый", "наружная", None), - ("110х2,7х500 SN2", 110, 2.7, 500, "оранжево-коричневый", "наружная", "SN2"), - ("110х2,7х1000 SN2", 110, 2.7, 1000, "оранжево-коричневый", "наружная", "SN2"), - ("110х2,7х2000 SN2", 110, 2.7, 2000, "оранжево-коричневый", "наружная", "SN2"), - ("110х2,7х3000 SN2", 110, 2.7, 3000, "оранжево-коричневый", "наружная", "SN2"), - ("110х2,7х4000 SN2", 110, 2.7, 4000, "оранжево-коричневый", "наружная", "SN2"), - ("110х3,2х500 SN4", 110, 3.2, 500, "оранжево-коричневый", "наружная", "SN4"), - ("110х3,2х1000 SN4", 110, 3.2, 1000, "оранжево-коричневый", "наружная", "SN4"), - ("110х3,2х2000 SN4", 110, 3.2, 2000, "оранжево-коричневый", "наружная", "SN4"), - ("110х3,2х3000 SN4", 110, 3.2, 3000, "оранжево-коричневый", "наружная", "SN4"), - ("110х3,2х4000 SN4", 110, 3.2, 4000, "оранжево-коричневый", "наружная", "SN4"), - ("160х3,6х1000 SN2", 160, 3.6, 1000, "оранжево-коричневый", "наружная", "SN2"), - ("160х3,6х2000 SN2", 160, 3.6, 2000, "оранжево-коричневый", "наружная", "SN2"), - ("160х3,6х3000 SN2", 160, 3.6, 3000, "оранжево-коричневый", "наружная", "SN2"), - ("160х4,0х1000 SN4", 160, 4.0, 1000, "оранжево-коричневый", "наружная", "SN4"), - ("160х4,0х2000 SN4", 160, 4.0, 2000, "оранжево-коричневый", "наружная", "SN4"), - ("160х4,0х3000 SN4", 160, 4.0, 3000, "оранжево-коричневый", "наружная", "SN4"), + ("110х2,3х500", 110, 2.3, 500, "оранжево-коричневый", "наружная", None, "RK111023500"), + ("110х2,3х1000", 110, 2.3, 1000, "оранжево-коричневый", "наружная", None, "RK1110231000"), + ("110х2,3х2000", 110, 2.3, 2000, "оранжево-коричневый", "наружная", None, "RK1110232000"), + ("110х2,3х3000", 110, 2.3, 3000, "оранжево-коричневый", "наружная", None, "RK1110233000"), + ("110х2,3х4000", 110, 2.3, 4000, "оранжево-коричневый", "наружная", None, "RK1110234000"), + ("110х2,7х500 SN2", 110, 2.7, 500, "оранжево-коричневый", "наружная", "SN2", "RK111027500"), + ("110х2,7х1000 SN2", 110, 2.7, 1000, "оранжево-коричневый", "наружная", "SN2", "RK1110271000"), + ("110х2,7х2000 SN2", 110, 2.7, 2000, "оранжево-коричневый", "наружная", "SN2", "RK1110272000"), + ("110х2,7х3000 SN2", 110, 2.7, 3000, "оранжево-коричневый", "наружная", "SN2", "RK1110273000"), + ("110х2,7х4000 SN2", 110, 2.7, 4000, "оранжево-коричневый", "наружная", "SN2", "RK1110274000"), + ("110х3,2х500 SN4", 110, 3.2, 500, "оранжево-коричневый", "наружная", "SN4", "RK111032500"), + ("110х3,2х1000 SN4", 110, 3.2, 1000, "оранжево-коричневый", "наружная", "SN4", "RK1110321000"), + ("110х3,2х2000 SN4", 110, 3.2, 2000, "оранжево-коричневый", "наружная", "SN4", "RK1110322000"), + ("110х3,2х3000 SN4", 110, 3.2, 3000, "оранжево-коричневый", "наружная", "SN4", "RK1110323000"), + ("110х3,2х4000 SN4", 110, 3.2, 4000, "оранжево-коричневый", "наружная", "SN4", "RK1110324000"), + ("160х3,6х1000 SN2", 160, 3.6, 1000, "оранжево-коричневый", "наружная", "SN2", "RK1160361000"), + ("160х3,6х2000 SN2", 160, 3.6, 2000, "оранжево-коричневый", "наружная", "SN2", "RK1160362000"), + ("160х3,6х3000 SN2", 160, 3.6, 3000, "оранжево-коричневый", "наружная", "SN2", "RK1160363000"), + ("160х4,0х1000 SN4", 160, 4.0, 1000, "оранжево-коричневый", "наружная", "SN4", "RK1160401000"), + ("160х4,0х2000 SN4", 160, 4.0, 2000, "оранжево-коричневый", "наружная", "SN4", "RK1160402000"), + ("160х4,0х3000 SN4", 160, 4.0, 3000, "оранжево-коричневый", "наружная", "SN4", "RK1160403000"), ] - for size, diameter, thickness, length, color, app_type, stiffness in external_pipes: + for size, diameter, thickness, length, color, app_type, stiffness, artikul in external_pipes: stiffness_text = f", класс жесткости {stiffness}" if stiffness else "" full_name = f"Труба НПВХ {diameter}x{thickness}x{length} мм наружная" # Замена на внешнюю ссылку @@ -162,7 +163,8 @@ def init_tables(db_name='pipe_store.db'): product_id = add_product( conn, full_name, external_category_id, image_url, - diameter, thickness, length, color, app_type, stiffness + diameter, thickness, length, color, app_type, stiffness, + artikul=artikul ) if product_id: added_products += 1 diff --git a/db/handlers/product_handler.py b/db/handlers/product_handler.py index 05397e9..00ec6b0 100644 --- a/db/handlers/product_handler.py +++ b/db/handlers/product_handler.py @@ -101,6 +101,7 @@ def get_products_with_filters( items.append({ "id": row['id'], "name": row['name'], + "slug": row['slug'], "description": row['description'], "categoryId": row['category_id'], "images": [row['image_path']] if row['image_path'] else ["/images/products/default.jpg"], @@ -144,7 +145,7 @@ def get_product_by_id(product_id: int, db_name: str = 'pipe_store.db') -> Option SELECT p.id, p.name, p.description, p.diameter, p.wall_thickness, p.length, p.color, p.application_type, p.stiffness_class, p.image_path, - p.created_at, p.category_id + p.slug, p.created_at, p.category_id FROM products p WHERE p.id = ? """, (product_id,)) @@ -192,6 +193,7 @@ def get_product_by_id(product_id: int, db_name: str = 'pipe_store.db') -> Option return { "id": row['id'], "name": row['name'], + "slug": row['slug'], "description": full_description, "price": None, "categoryId": row['category_id'], @@ -207,6 +209,82 @@ def get_product_by_id(product_id: int, db_name: str = 'pipe_store.db') -> Option finally: conn.close() +def get_product_by_slug(slug: str, db_name: str = 'pipe_store.db') -> Optional[Dict[str, Any]]: + """ + Получить товар по slug с полной информацией + """ + conn = sqlite3.connect(db_name) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT p.id, p.name, p.description, + p.diameter, p.wall_thickness, p.length, p.color, + p.application_type, p.stiffness_class, p.image_path, + p.slug, p.created_at, p.category_id + FROM products p + WHERE p.slug = ? + """, (slug,)) + + row = cursor.fetchone() + if not row: + return None + + product_id = row['id'] + + images = get_product_images(product_id, row['image_path'], conn) + + cursor.execute(""" + SELECT filter_key, value + FROM product_filters + WHERE product_id = ? + """, (product_id,)) + + extra_attributes = {} + for filter_row in cursor.fetchall(): + extra_attributes[filter_row['filter_key']] = filter_row['value'] + + full_description = generate_full_description(row) + + size = None + if row['diameter'] and row['wall_thickness'] and row['length'] and row['length'] > 0: + size = f"{row['diameter']}x{row['wall_thickness']}x{row['length']}" + amount = get_pack_amount(product_id, row['diameter']) + + attributes = { + "color": row['color'], + "amount": amount, + "size": size, + "diameter": str(row['diameter']) if row['diameter'] else None, + "wall_thickness": str(row['wall_thickness']) if row['wall_thickness'] else None, + "length": str(row['length']) if row['length'] and row['length'] > 0 else None, + "stiffness_class": row['stiffness_class'], + "application_type": row['application_type'], + } + + attributes.update(extra_attributes) + + return { + "id": row['id'], + "name": row['name'], + "slug": row['slug'], + "description": full_description, + "price": None, + "categoryId": row['category_id'], + "images": images, + "attributes": attributes, + "inStock": check_stock(product_id, conn), + "created_at": row['created_at'] + } + + except sqlite3.Error as e: + log.error(f"Ошибка при получении товара по slug {slug}: {e}") + return None + finally: + conn.close() + + def get_product_images(product_id: int, main_image: Optional[str], conn: sqlite3.Connection) -> List[str]: """Получить список изображений товара""" images = [] @@ -278,6 +356,7 @@ def get_product_list(page=1, limit=100, category_id=None): products.append({ 'id': row['id'], 'name': row['name'], + 'slug': row['slug'], 'description': row['description'] }) return products