|
""" |
|
增強的商品查詢服務 - 移植自 inventory_proj_routers |
|
提供更精確的商品搜尋和庫存查詢功能 |
|
""" |
|
|
|
import logging |
|
from typing import List, Optional, Dict, Any |
|
from sqlalchemy.orm import Session, joinedload |
|
from sqlalchemy import or_, and_, func |
|
from backend.database.connection import get_database_session, close_database_session |
|
from backend.database.models import Product, Category, PurchaseOrder, SalesOrder |
|
from backend.models.schemas import DatabaseResult |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
class EnhancedProductService: |
|
"""增強的商品查詢服務""" |
|
|
|
def __init__(self): |
|
pass |
|
|
|
def search_products_advanced( |
|
self, |
|
query_text: str = None, |
|
category_name: str = None, |
|
warehouse: str = None, |
|
include_stock_info: bool = True, |
|
min_stock: int = None, |
|
max_stock: int = None, |
|
limit: int = 20 |
|
) -> DatabaseResult: |
|
""" |
|
進階商品搜尋功能 |
|
|
|
Args: |
|
query_text: 搜尋關鍵字(商品名稱、編號、條碼) |
|
category_name: 分類名稱 |
|
warehouse: 倉庫名稱 |
|
include_stock_info: 是否包含庫存資訊 |
|
min_stock: 最小庫存量 |
|
max_stock: 最大庫存量 |
|
limit: 查詢限制數量 |
|
""" |
|
db = None |
|
try: |
|
logger.info(f"🔍 開始進階商品搜尋: '{query_text}'") |
|
db = get_database_session() |
|
|
|
|
|
query = db.query(Product).options( |
|
joinedload(Product.category) |
|
).filter(Product.is_deleted == False) |
|
|
|
|
|
if query_text: |
|
logger.info(f"🔑 使用關鍵字搜尋: '{query_text}'") |
|
|
|
|
|
search_terms = self._extract_keywords(query_text) |
|
logger.info(f"📝 智能提取的關鍵字: {search_terms}") |
|
|
|
if search_terms: |
|
|
|
search_filters = [] |
|
for term in search_terms: |
|
search_filters.extend([ |
|
Product.productName.ilike(f"%{term}%"), |
|
Product.productCode.ilike(f"%{term}%"), |
|
Product.barcode.ilike(f"%{term}%") |
|
]) |
|
|
|
if search_filters: |
|
query = query.filter(or_(*search_filters)) |
|
logger.info(f"✅ 應用了 {len(search_filters)} 個搜尋條件") |
|
else: |
|
logger.warning(f"⚠️ 沒有提取到有效關鍵字") |
|
|
|
|
|
if category_name: |
|
query = query.join(Category).filter( |
|
Category.name.ilike(f"%{category_name}%") |
|
) |
|
|
|
|
|
if warehouse: |
|
query = query.filter(Product.warehouse.ilike(f"%{warehouse}%")) |
|
|
|
|
|
if min_stock is not None: |
|
query = query.filter(Product.stock >= min_stock) |
|
if max_stock is not None: |
|
query = query.filter(Product.stock <= max_stock) |
|
|
|
|
|
products = query.limit(limit).all() |
|
|
|
|
|
data = [] |
|
for product in products: |
|
product_data = { |
|
"id": product.id, |
|
"product_code": product.productCode, |
|
"product_name": product.productName, |
|
"unit": product.unit, |
|
"warehouse": product.warehouse, |
|
"unit_weight": product.unitWeight, |
|
"barcode": product.barcode, |
|
"category_id": product.category_id, |
|
"category_name": product.category.name if product.category else None, |
|
"created_at": product.createdAt.isoformat() if product.createdAt else None, |
|
"updated_at": product.updatedAt.isoformat() if product.updatedAt else None |
|
} |
|
|
|
|
|
if include_stock_info: |
|
product_data.update({ |
|
"current_stock": product.stock, |
|
"stock_status": self._get_stock_status(product.stock), |
|
"is_low_stock": product.stock <= 10 |
|
}) |
|
|
|
data.append(product_data) |
|
|
|
return DatabaseResult( |
|
success=True, |
|
data=data, |
|
count=len(data) |
|
) |
|
|
|
except Exception as e: |
|
logger.error(f"進階商品搜尋錯誤: {str(e)}") |
|
return DatabaseResult( |
|
success=False, |
|
error=f"商品搜尋失敗: {str(e)}" |
|
) |
|
finally: |
|
if db: |
|
close_database_session(db) |
|
|
|
def get_products_by_category(self, category_name: str, limit: int = 20) -> DatabaseResult: |
|
"""根據分類獲取商品""" |
|
db = None |
|
try: |
|
db = get_database_session() |
|
|
|
products = db.query(Product).join(Category).filter( |
|
and_( |
|
Category.name.ilike(f"%{category_name}%"), |
|
Product.is_deleted == False |
|
) |
|
).options(joinedload(Product.category)).limit(limit).all() |
|
|
|
data = [] |
|
for product in products: |
|
data.append({ |
|
"id": product.id, |
|
"product_code": product.productCode, |
|
"product_name": product.productName, |
|
"current_stock": product.stock, |
|
"unit": product.unit, |
|
"category_name": product.category.name if product.category else None, |
|
"warehouse": product.warehouse |
|
}) |
|
|
|
return DatabaseResult( |
|
success=True, |
|
data=data, |
|
count=len(data) |
|
) |
|
|
|
except Exception as e: |
|
logger.error(f"分類商品查詢錯誤: {str(e)}") |
|
return DatabaseResult( |
|
success=False, |
|
error=f"分類商品查詢失敗: {str(e)}" |
|
) |
|
finally: |
|
if db: |
|
close_database_session(db) |
|
|
|
def get_low_stock_products(self, threshold: int = 10) -> DatabaseResult: |
|
"""獲取低庫存商品""" |
|
db = None |
|
try: |
|
db = get_database_session() |
|
|
|
products = db.query(Product).filter( |
|
and_( |
|
Product.stock <= threshold, |
|
Product.is_deleted == False |
|
) |
|
).options(joinedload(Product.category)).all() |
|
|
|
data = [] |
|
for product in products: |
|
data.append({ |
|
"id": product.id, |
|
"product_code": product.productCode, |
|
"product_name": product.productName, |
|
"current_stock": product.stock, |
|
"threshold": threshold, |
|
"unit": product.unit, |
|
"category_name": product.category.name if product.category else None, |
|
"warehouse": product.warehouse, |
|
"urgency_level": self._get_urgency_level(product.stock) |
|
}) |
|
|
|
return DatabaseResult( |
|
success=True, |
|
data=data, |
|
count=len(data) |
|
) |
|
|
|
except Exception as e: |
|
logger.error(f"低庫存查詢錯誤: {str(e)}") |
|
return DatabaseResult( |
|
success=False, |
|
error=f"低庫存查詢失敗: {str(e)}" |
|
) |
|
finally: |
|
if db: |
|
close_database_session(db) |
|
|
|
def get_product_recommendations(self, query_text: str, limit: int = 5) -> DatabaseResult: |
|
""" |
|
商品推薦功能 - 基於關鍵字的智能推薦 |
|
特別針對像 "推薦貓砂" 這樣的查詢 |
|
""" |
|
db = None |
|
try: |
|
logger.info(f"🛍️ 開始商品推薦查詢: '{query_text}'") |
|
db = get_database_session() |
|
|
|
if not db: |
|
logger.error(f"❌ 無法獲取資料庫連接") |
|
return DatabaseResult(success=False, error="資料庫連接失敗") |
|
|
|
logger.info(f"✅ 資料庫連接成功") |
|
|
|
|
|
try: |
|
total_products = db.query(Product).filter(Product.is_deleted == False).count() |
|
logger.info(f"📊 資料庫中共有 {total_products} 個有效商品") |
|
except Exception as e: |
|
logger.error(f"❌ 基本查詢失敗: {str(e)}") |
|
return DatabaseResult(success=False, error=f"基本查詢失敗: {str(e)}") |
|
|
|
|
|
keywords = self._extract_keywords(query_text) |
|
logger.info(f"🔑 推薦查詢關鍵字: {keywords}") |
|
|
|
|
|
query = db.query(Product).filter(Product.is_deleted == False) |
|
logger.info(f"📊 基礎查詢建立完成") |
|
|
|
|
|
if keywords: |
|
search_filters = [] |
|
for keyword in keywords: |
|
search_filters.extend([ |
|
Product.productName.ilike(f"%{keyword}%"), |
|
Product.productCode.ilike(f"%{keyword}%"), |
|
Product.barcode.ilike(f"%{keyword}%") |
|
]) |
|
|
|
logger.info(f"🔍 建立了 {len(search_filters)} 個搜尋條件") |
|
logger.info(f"🔑 前5個搜尋條件: {[f'productName ILIKE %{kw}%' for kw in keywords[:5]]}") |
|
|
|
|
|
if search_filters: |
|
query = query.filter(or_(*search_filters)) |
|
logger.info(f"✅ 搜尋條件已應用到查詢") |
|
|
|
|
|
try: |
|
test_count = query.count() |
|
logger.info(f"📊 符合條件的商品數量: {test_count}") |
|
except Exception as e: |
|
logger.error(f"❌ 查詢計數失敗: {str(e)}") |
|
return DatabaseResult(success=False, error=f"查詢執行失敗: {str(e)}") |
|
else: |
|
logger.warning(f"⚠️ 沒有搜尋條件") |
|
else: |
|
logger.warning(f"⚠️ 沒有關鍵字,將返回所有商品") |
|
|
|
|
|
query = query.order_by(Product.stock.desc()) |
|
|
|
try: |
|
products = query.options(joinedload(Product.category)).limit(limit).all() |
|
logger.info(f"📦 成功查詢到 {len(products)} 個商品") |
|
|
|
|
|
for product in products: |
|
logger.info(f"📦 商品詳情: {product.productName} (編號: {product.productCode}, 庫存: {product.stock})") |
|
|
|
except Exception as e: |
|
logger.error(f"❌ 商品查詢執行失敗: {str(e)}") |
|
import traceback |
|
logger.error(f"📋 查詢錯誤詳情: {traceback.format_exc()}") |
|
return DatabaseResult(success=False, error=f"商品查詢執行失敗: {str(e)}") |
|
|
|
data = [] |
|
for product in products: |
|
|
|
matched_keywords = [] |
|
for keyword in keywords: |
|
if (keyword.lower() in product.productName.lower() or |
|
keyword.lower() in product.productCode.lower()): |
|
matched_keywords.append(keyword) |
|
|
|
logger.info(f"📦 商品: {product.productName} - 匹配關鍵字: {matched_keywords}") |
|
|
|
data.append({ |
|
"id": product.id, |
|
"product_code": product.productCode, |
|
"product_name": product.productName, |
|
"current_stock": product.stock, |
|
"unit": product.unit, |
|
"category_name": product.category.name if product.category else None, |
|
"warehouse": product.warehouse, |
|
"recommendation_reason": f"符合關鍵字: {', '.join(matched_keywords[:3]) if matched_keywords else '一般推薦'}", |
|
"availability": "有庫存" if product.stock > 0 else "缺貨" |
|
}) |
|
|
|
logger.info(f"✅ 推薦查詢完成,返回 {len(data)} 個商品") |
|
|
|
return DatabaseResult( |
|
success=True, |
|
data=data, |
|
count=len(data) |
|
) |
|
|
|
except Exception as e: |
|
logger.error(f"❌ 商品推薦錯誤: {str(e)}") |
|
import traceback |
|
logger.error(f"📋 錯誤詳情: {traceback.format_exc()}") |
|
return DatabaseResult( |
|
success=False, |
|
error=f"商品推薦失敗: {str(e)}" |
|
) |
|
finally: |
|
if db: |
|
logger.info(f"🔒 關閉資料庫連接") |
|
close_database_session(db) |
|
else: |
|
logger.warning(f"⚠️ 沒有資料庫連接需要關閉") |
|
|
|
def _get_stock_status(self, stock: int) -> str: |
|
"""獲取庫存狀態""" |
|
if stock <= 0: |
|
return "缺貨" |
|
elif stock <= 5: |
|
return "庫存極低" |
|
elif stock <= 10: |
|
return "庫存偏低" |
|
elif stock <= 50: |
|
return "庫存正常" |
|
else: |
|
return "庫存充足" |
|
|
|
def _get_urgency_level(self, stock: int) -> str: |
|
"""獲取緊急程度""" |
|
if stock <= 0: |
|
return "緊急" |
|
elif stock <= 3: |
|
return "高" |
|
elif stock <= 10: |
|
return "中" |
|
else: |
|
return "低" |
|
|
|
def _extract_keywords(self, query_text: str) -> List[str]: |
|
"""從查詢文字中提取關鍵字,並擴展相關詞彙""" |
|
|
|
stop_words = ['推薦', '有沒有', '是否有', '請問', '想要', '需要', '找', '查詢', '搜尋', '還有嗎', '還有', '嗎', '可以'] |
|
|
|
|
|
cleaned_text = query_text.replace('?', '').replace('?', '').strip() |
|
|
|
|
|
core_product_words = ['貓砂', '狗糧', '寵物', '商品', '產品', '貓', '狗', '犬', '礦砂'] |
|
extracted_core_words = [] |
|
|
|
for core_word in core_product_words: |
|
if core_word in cleaned_text: |
|
extracted_core_words.append(core_word) |
|
|
|
|
|
words = cleaned_text.split() |
|
keywords = [] |
|
|
|
for word in words: |
|
if word not in stop_words and len(word) > 1: |
|
keywords.append(word) |
|
|
|
|
|
all_keywords = list(set(extracted_core_words + keywords)) |
|
|
|
|
|
if not all_keywords: |
|
all_keywords = [cleaned_text] |
|
|
|
|
|
expanded_keywords = [] |
|
for keyword in all_keywords: |
|
expanded_keywords.append(keyword) |
|
|
|
|
|
if '貓砂' in keyword or '貓' in keyword: |
|
expanded_keywords.extend(['礦砂', '豆腐砂', '水晶砂', '木屑砂', 'litter', '貓砂']) |
|
|
|
|
|
if '狗糧' in keyword or '狗' in keyword: |
|
expanded_keywords.extend(['犬糧', '犬種', '狗食', 'dog']) |
|
|
|
|
|
if '寵物' in keyword: |
|
expanded_keywords.extend(['貓', '狗', '犬', 'pet', 'cat']) |
|
|
|
|
|
if '商品' in keyword or '產品' in keyword: |
|
expanded_keywords.extend(['貓砂', '狗糧', '寵物', '食品', '用品']) |
|
|
|
|
|
unique_keywords = list(set(expanded_keywords)) |
|
|
|
|
|
logger.info(f"關鍵字提取: '{query_text}' → 核心詞: {extracted_core_words} → 最終: {unique_keywords}") |
|
|
|
return unique_keywords |
|
|