Commit
·
68c8519
1
Parent(s):
89879a0
修正訂單查詢問題
Browse files- backend/services/database_service.py +140 -20
- backend/services/nlp_service.py +26 -7
- test_order_queries.py +272 -0
- test_search_orders.py +255 -0
backend/services/database_service.py
CHANGED
@@ -567,40 +567,85 @@ class DatabaseService:
|
|
567 |
if db:
|
568 |
close_database_session(db)
|
569 |
|
570 |
-
def search_orders(self, user_id: str = None, status: str = None,
|
571 |
-
|
|
|
|
|
572 |
db = None
|
573 |
try:
|
574 |
db = get_database_session()
|
575 |
-
|
576 |
-
# 使用 SalesOrder
|
577 |
-
query = db.query(SalesOrder)
|
578 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
579 |
# 用戶篩選 (銷售人員)
|
580 |
if user_id:
|
581 |
query = query.filter(SalesOrder.salesperson_id == user_id)
|
582 |
-
|
583 |
-
#
|
|
|
|
|
|
|
|
|
584 |
if status:
|
585 |
-
|
586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
587 |
orders = query.limit(limit).all()
|
588 |
-
|
589 |
# 轉換為字典格式
|
590 |
order_data = []
|
591 |
for order in orders:
|
|
|
|
|
|
|
592 |
order_info = {
|
593 |
"order_id": order.so_number,
|
594 |
"sales_date": order.sales_date.isoformat() if order.sales_date else None,
|
595 |
-
"
|
596 |
-
"
|
597 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
598 |
"total_amount": float(order.total_amount) if order.total_amount else 0.0,
|
|
|
599 |
"created_at": order.created_at.isoformat() if order.created_at else None,
|
600 |
"updated_at": order.updated_at.isoformat() if order.updated_at else None
|
601 |
}
|
602 |
order_data.append(order_info)
|
603 |
-
|
604 |
return DatabaseResult(
|
605 |
success=True,
|
606 |
data=order_data,
|
@@ -617,6 +662,23 @@ class DatabaseService:
|
|
617 |
if db:
|
618 |
close_database_session(db)
|
619 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
620 |
def get_low_stock_products(self, threshold: int = 10) -> DatabaseResult:
|
621 |
"""低庫存商品查詢"""
|
622 |
db = None
|
@@ -719,8 +781,12 @@ class DatabaseService:
|
|
719 |
return self.search_products(query_text=product_name)
|
720 |
|
721 |
# 訂單查詢
|
722 |
-
elif any(keyword in message_lower for keyword in ['訂單', '訂購', '購買']):
|
723 |
-
|
|
|
|
|
|
|
|
|
724 |
|
725 |
# 低庫存查詢
|
726 |
elif any(keyword in message_lower for keyword in ['低庫存', '缺貨', '不足']):
|
@@ -746,9 +812,63 @@ class DatabaseService:
|
|
746 |
# 簡單的商品名稱提取邏輯
|
747 |
# 移除常見的查詢關鍵字
|
748 |
keywords_to_remove = ['查詢', '搜尋', '找', '商品', '產品', '庫存', '有沒有', '請問']
|
749 |
-
|
750 |
cleaned_message = message
|
751 |
for keyword in keywords_to_remove:
|
752 |
cleaned_message = cleaned_message.replace(keyword, '')
|
753 |
-
|
754 |
-
return cleaned_message.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
567 |
if db:
|
568 |
close_database_session(db)
|
569 |
|
570 |
+
def search_orders(self, user_id: str = None, status: str = None, order_number: str = None,
|
571 |
+
customer_id: str = None, date_from: str = None, date_to: str = None,
|
572 |
+
limit: int = 10) -> DatabaseResult:
|
573 |
+
"""增強的訂單查詢功能"""
|
574 |
db = None
|
575 |
try:
|
576 |
db = get_database_session()
|
577 |
+
|
578 |
+
# 使用 SalesOrder 作為主要的訂單查詢,預載入關聯資料
|
579 |
+
query = db.query(SalesOrder).options(
|
580 |
+
joinedload(SalesOrder.customer),
|
581 |
+
joinedload(SalesOrder.salesperson)
|
582 |
+
)
|
583 |
+
|
584 |
+
# 訂單編號查詢 (支援 SO- 格式)
|
585 |
+
if order_number:
|
586 |
+
if not order_number.startswith('SO-'):
|
587 |
+
# 如果用戶只輸入部分編號,自動補全
|
588 |
+
order_number = f"SO-{order_number}"
|
589 |
+
query = query.filter(SalesOrder.so_number.ilike(f"%{order_number}%"))
|
590 |
+
|
591 |
# 用戶篩選 (銷售人員)
|
592 |
if user_id:
|
593 |
query = query.filter(SalesOrder.salesperson_id == user_id)
|
594 |
+
|
595 |
+
# 客戶篩選
|
596 |
+
if customer_id:
|
597 |
+
query = query.filter(SalesOrder.customer_id == customer_id)
|
598 |
+
|
599 |
+
# 狀態篩選 (支援中文和英文)
|
600 |
if status:
|
601 |
+
status_mapping = {
|
602 |
+
'已交付': 'DELIVERED',
|
603 |
+
'已出貨': 'SHIPPED',
|
604 |
+
'處理中': 'PROCESSING',
|
605 |
+
'已取消': 'CANCELLED',
|
606 |
+
'待處理': 'PENDING'
|
607 |
+
}
|
608 |
+
# 檢查是否為中文狀態,轉換為英文
|
609 |
+
english_status = status_mapping.get(status, status)
|
610 |
+
query = query.filter(SalesOrder.status.ilike(f"%{english_status}%"))
|
611 |
+
|
612 |
+
# 日期範圍篩選
|
613 |
+
if date_from:
|
614 |
+
query = query.filter(SalesOrder.sales_date >= date_from)
|
615 |
+
if date_to:
|
616 |
+
query = query.filter(SalesOrder.sales_date <= date_to)
|
617 |
+
|
618 |
+
# 按日期降序排列,最新的在前面
|
619 |
+
query = query.order_by(SalesOrder.sales_date.desc())
|
620 |
+
|
621 |
orders = query.limit(limit).all()
|
622 |
+
|
623 |
# 轉換為字典格式
|
624 |
order_data = []
|
625 |
for order in orders:
|
626 |
+
# 狀態中文化
|
627 |
+
status_display = self._get_order_status_display(order.status)
|
628 |
+
|
629 |
order_info = {
|
630 |
"order_id": order.so_number,
|
631 |
"sales_date": order.sales_date.isoformat() if order.sales_date else None,
|
632 |
+
"customer_id": order.customer_id,
|
633 |
+
"customer_name": order.customer.customerName if order.customer else "未知客戶",
|
634 |
+
"salesperson_id": order.salesperson_id,
|
635 |
+
"salesperson_name": order.salesperson.name if order.salesperson else "未指定",
|
636 |
+
"status": order.status,
|
637 |
+
"status_display": status_display,
|
638 |
+
"payment_term": order.payment_term,
|
639 |
+
"subtotal": float(order.subtotal) if order.subtotal else 0.0,
|
640 |
+
"tax_amount": float(order.tax_amount) if order.tax_amount else 0.0,
|
641 |
+
"discount_amount": float(order.discount_amount) if order.discount_amount else 0.0,
|
642 |
"total_amount": float(order.total_amount) if order.total_amount else 0.0,
|
643 |
+
"notes": order.notes or "",
|
644 |
"created_at": order.created_at.isoformat() if order.created_at else None,
|
645 |
"updated_at": order.updated_at.isoformat() if order.updated_at else None
|
646 |
}
|
647 |
order_data.append(order_info)
|
648 |
+
|
649 |
return DatabaseResult(
|
650 |
success=True,
|
651 |
data=order_data,
|
|
|
662 |
if db:
|
663 |
close_database_session(db)
|
664 |
|
665 |
+
def _get_order_status_display(self, status) -> str:
|
666 |
+
"""將訂單狀態轉換為中文顯示"""
|
667 |
+
status_mapping = {
|
668 |
+
'DELIVERED': '已交付',
|
669 |
+
'SHIPPED': '已出貨',
|
670 |
+
'PROCESSING': '處理中',
|
671 |
+
'CANCELLED': '已取消',
|
672 |
+
'PENDING': '待處理',
|
673 |
+
'CONFIRMED': '已確認',
|
674 |
+
'COMPLETED': '已完成'
|
675 |
+
}
|
676 |
+
|
677 |
+
if hasattr(status, 'value'):
|
678 |
+
return status_mapping.get(status.value, str(status.value))
|
679 |
+
else:
|
680 |
+
return status_mapping.get(str(status), str(status))
|
681 |
+
|
682 |
def get_low_stock_products(self, threshold: int = 10) -> DatabaseResult:
|
683 |
"""低庫存商品查詢"""
|
684 |
db = None
|
|
|
781 |
return self.search_products(query_text=product_name)
|
782 |
|
783 |
# 訂單查詢
|
784 |
+
elif any(keyword in message_lower for keyword in ['訂單', '訂購', '購買', 'so-']):
|
785 |
+
# 提取訂單編號
|
786 |
+
order_number = self._extract_order_number(user_message)
|
787 |
+
# 提取狀態
|
788 |
+
status = self._extract_order_status(user_message)
|
789 |
+
return self.search_orders(user_id=user_id, order_number=order_number, status=status)
|
790 |
|
791 |
# 低庫存查詢
|
792 |
elif any(keyword in message_lower for keyword in ['低庫存', '缺貨', '不足']):
|
|
|
812 |
# 簡單的商品名稱提取邏輯
|
813 |
# 移除常見的查詢關鍵字
|
814 |
keywords_to_remove = ['查詢', '搜尋', '找', '商品', '產品', '庫存', '有沒有', '請問']
|
815 |
+
|
816 |
cleaned_message = message
|
817 |
for keyword in keywords_to_remove:
|
818 |
cleaned_message = cleaned_message.replace(keyword, '')
|
819 |
+
|
820 |
+
return cleaned_message.strip()
|
821 |
+
|
822 |
+
def _extract_order_number(self, message: str) -> str:
|
823 |
+
"""從訊息中提取訂單編號"""
|
824 |
+
import re
|
825 |
+
|
826 |
+
# 尋找 SO- 格式的訂單編號
|
827 |
+
so_pattern = r'SO-\d{8}-\d{3}'
|
828 |
+
match = re.search(so_pattern, message.upper())
|
829 |
+
if match:
|
830 |
+
return match.group()
|
831 |
+
|
832 |
+
# 尋找日期格式的編號 (20250706-001)
|
833 |
+
date_pattern = r'\d{8}-\d{3}'
|
834 |
+
match = re.search(date_pattern, message)
|
835 |
+
if match:
|
836 |
+
return f"SO-{match.group()}"
|
837 |
+
|
838 |
+
# 尋找純數字編號
|
839 |
+
number_pattern = r'\d{6,}'
|
840 |
+
match = re.search(number_pattern, message)
|
841 |
+
if match:
|
842 |
+
return match.group()
|
843 |
+
|
844 |
+
return None
|
845 |
+
|
846 |
+
def _extract_order_status(self, message: str) -> str:
|
847 |
+
"""從訊息中提取訂單狀態"""
|
848 |
+
message_lower = message.lower()
|
849 |
+
|
850 |
+
# 中文狀態關鍵字
|
851 |
+
status_keywords = {
|
852 |
+
'已交付': 'DELIVERED',
|
853 |
+
'已出貨': 'SHIPPED',
|
854 |
+
'出貨': 'SHIPPED',
|
855 |
+
'交付': 'DELIVERED',
|
856 |
+
'處理中': 'PROCESSING',
|
857 |
+
'已取消': 'CANCELLED',
|
858 |
+
'取消': 'CANCELLED',
|
859 |
+
'待處理': 'PENDING',
|
860 |
+
'已確認': 'CONFIRMED',
|
861 |
+
'已完成': 'COMPLETED'
|
862 |
+
}
|
863 |
+
|
864 |
+
for chinese, english in status_keywords.items():
|
865 |
+
if chinese in message_lower:
|
866 |
+
return english
|
867 |
+
|
868 |
+
# 英文狀態關鍵字
|
869 |
+
english_keywords = ['delivered', 'shipped', 'processing', 'cancelled', 'pending', 'confirmed', 'completed']
|
870 |
+
for keyword in english_keywords:
|
871 |
+
if keyword in message_lower:
|
872 |
+
return keyword.upper()
|
873 |
+
|
874 |
+
return None
|
backend/services/nlp_service.py
CHANGED
@@ -612,19 +612,38 @@ class NLPService:
|
|
612 |
return response
|
613 |
|
614 |
def _format_order_response(self, data: List[Dict[str, Any]]) -> str:
|
615 |
-
"""格式化訂單查詢回應"""
|
|
|
|
|
|
|
616 |
if len(data) == 1:
|
617 |
order = data[0]
|
618 |
-
|
|
|
619 |
f"訂單編號:{order.get('order_id', 'N/A')}\n" \
|
620 |
-
f"
|
|
|
|
|
|
|
|
|
621 |
f"總金額:${order.get('total_amount', 0)}\n" \
|
622 |
-
f"
|
623 |
else:
|
624 |
-
response = f"找到 {len(data)} 筆訂單:\n"
|
625 |
for i, order in enumerate(data[:5], 1):
|
626 |
-
|
627 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
628 |
|
629 |
def _format_low_stock_response(self, data: List[Dict[str, Any]]) -> str:
|
630 |
"""格式化低庫存警告回應"""
|
|
|
612 |
return response
|
613 |
|
614 |
def _format_order_response(self, data: List[Dict[str, Any]]) -> str:
|
615 |
+
"""格式化訂單查詢回應 - 適配銷售訂單資料結構"""
|
616 |
+
if not data:
|
617 |
+
return "沒有找到符合條件的訂單。"
|
618 |
+
|
619 |
if len(data) == 1:
|
620 |
order = data[0]
|
621 |
+
status_display = order.get('status_display', order.get('status', 'N/A'))
|
622 |
+
return f"📋 訂單詳細資訊:\n" \
|
623 |
f"訂單編號:{order.get('order_id', 'N/A')}\n" \
|
624 |
+
f"銷售日期:{order.get('sales_date', 'N/A')}\n" \
|
625 |
+
f"狀態:{status_display}\n" \
|
626 |
+
f"客戶:{order.get('customer_name', 'N/A')}\n" \
|
627 |
+
f"銷售人員:{order.get('salesperson_name', 'N/A')}\n" \
|
628 |
+
f"付款條件:{order.get('payment_term', 'N/A')}\n" \
|
629 |
f"總金額:${order.get('total_amount', 0)}\n" \
|
630 |
+
f"更新時間:{order.get('updated_at', 'N/A')[:10] if order.get('updated_at') else 'N/A'}"
|
631 |
else:
|
632 |
+
response = f"📋 找到 {len(data)} 筆訂單:\n\n"
|
633 |
for i, order in enumerate(data[:5], 1):
|
634 |
+
status_display = order.get('status_display', order.get('status', 'N/A'))
|
635 |
+
sales_date = order.get('sales_date', 'N/A')
|
636 |
+
if isinstance(sales_date, str) and len(sales_date) > 10:
|
637 |
+
sales_date = sales_date[:10] # 只顯示日期部分
|
638 |
+
|
639 |
+
response += f"{i}. {order.get('order_id', 'N/A')}\n"
|
640 |
+
response += f" 狀態:{status_display} | 日期:{sales_date}\n"
|
641 |
+
response += f" 客戶:{order.get('customer_name', 'N/A')} | 金額:${order.get('total_amount', 0)}\n\n"
|
642 |
+
|
643 |
+
if len(data) > 5:
|
644 |
+
response += f"... 還有 {len(data) - 5} 筆訂單"
|
645 |
+
|
646 |
+
return response.strip()
|
647 |
|
648 |
def _format_low_stock_response(self, data: List[Dict[str, Any]]) -> str:
|
649 |
"""格式化低庫存警告回應"""
|
test_order_queries.py
ADDED
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
測試訂單查詢功能 - 基於實際銷售訂單資料
|
3 |
+
"""
|
4 |
+
|
5 |
+
def test_order_number_extraction():
|
6 |
+
"""測試訂單編號提取功能"""
|
7 |
+
|
8 |
+
def extract_order_number_simple(message: str) -> str:
|
9 |
+
"""簡化版訂單編號提取"""
|
10 |
+
import re
|
11 |
+
|
12 |
+
# 尋找 SO- 格式的訂單編號
|
13 |
+
so_pattern = r'SO-\d{8}-\d{3}'
|
14 |
+
match = re.search(so_pattern, message.upper())
|
15 |
+
if match:
|
16 |
+
return match.group()
|
17 |
+
|
18 |
+
# 尋找日期格式的編號 (20250706-001)
|
19 |
+
date_pattern = r'\d{8}-\d{3}'
|
20 |
+
match = re.search(date_pattern, message)
|
21 |
+
if match:
|
22 |
+
return f"SO-{match.group()}"
|
23 |
+
|
24 |
+
# 尋找純數字編號
|
25 |
+
number_pattern = r'\d{6,}'
|
26 |
+
match = re.search(number_pattern, message)
|
27 |
+
if match:
|
28 |
+
return match.group()
|
29 |
+
|
30 |
+
return None
|
31 |
+
|
32 |
+
print("🔍 測試訂單編號提取")
|
33 |
+
print("=" * 50)
|
34 |
+
|
35 |
+
test_queries = [
|
36 |
+
"查詢訂單 SO-20250706-001",
|
37 |
+
"我的訂單 SO-20250708-001 狀態如何?",
|
38 |
+
"20250706-001 這個訂單",
|
39 |
+
"訂單編號 20250708-001",
|
40 |
+
"SO20250706001", # 沒有連字符
|
41 |
+
"查詢我的訂單", # 沒有編號
|
42 |
+
]
|
43 |
+
|
44 |
+
for query in test_queries:
|
45 |
+
order_number = extract_order_number_simple(query)
|
46 |
+
print(f"'{query}' → 訂單編號: {order_number}")
|
47 |
+
|
48 |
+
def test_order_status_extraction():
|
49 |
+
"""測試訂單狀態提取功能"""
|
50 |
+
|
51 |
+
def extract_order_status_simple(message: str) -> str:
|
52 |
+
"""簡化版訂單狀態提取"""
|
53 |
+
message_lower = message.lower()
|
54 |
+
|
55 |
+
# 中文狀態關鍵字
|
56 |
+
status_keywords = {
|
57 |
+
'已交付': 'DELIVERED',
|
58 |
+
'已出貨': 'SHIPPED',
|
59 |
+
'出貨': 'SHIPPED',
|
60 |
+
'交付': 'DELIVERED',
|
61 |
+
'處理中': 'PROCESSING',
|
62 |
+
'已取消': 'CANCELLED',
|
63 |
+
'取消': 'CANCELLED',
|
64 |
+
'待處理': 'PENDING',
|
65 |
+
'已確認': 'CONFIRMED',
|
66 |
+
'已完成': 'COMPLETED'
|
67 |
+
}
|
68 |
+
|
69 |
+
for chinese, english in status_keywords.items():
|
70 |
+
if chinese in message_lower:
|
71 |
+
return english
|
72 |
+
|
73 |
+
# 英文狀態關鍵字
|
74 |
+
english_keywords = ['delivered', 'shipped', 'processing', 'cancelled', 'pending', 'confirmed', 'completed']
|
75 |
+
for keyword in english_keywords:
|
76 |
+
if keyword in message_lower:
|
77 |
+
return keyword.upper()
|
78 |
+
|
79 |
+
return None
|
80 |
+
|
81 |
+
print("\n📊 測試訂單狀態提取")
|
82 |
+
print("=" * 50)
|
83 |
+
|
84 |
+
test_queries = [
|
85 |
+
"查詢已交付的訂單",
|
86 |
+
"已出貨訂單有哪些?",
|
87 |
+
"處理中的訂單",
|
88 |
+
"DELIVERED 狀態的訂單",
|
89 |
+
"shipped orders",
|
90 |
+
"我的訂單狀態", # 沒有狀態
|
91 |
+
]
|
92 |
+
|
93 |
+
for query in test_queries:
|
94 |
+
status = extract_order_status_simple(query)
|
95 |
+
print(f"'{query}' → 狀態: {status}")
|
96 |
+
|
97 |
+
def test_order_query_simulation():
|
98 |
+
"""模擬訂單查詢功能"""
|
99 |
+
|
100 |
+
# 模擬的銷售訂單資料(來自您的 Supabase)
|
101 |
+
orders = [
|
102 |
+
{
|
103 |
+
"id": 1,
|
104 |
+
"so_number": "SO-20250706-001",
|
105 |
+
"sales_date": "2025-07-06",
|
106 |
+
"customer_id": 1,
|
107 |
+
"salesperson_id": 1,
|
108 |
+
"payment_term": "MONTHLY",
|
109 |
+
"status": "DELIVERED",
|
110 |
+
"subtotal": 0,
|
111 |
+
"tax_amount": 0,
|
112 |
+
"discount_amount": 0,
|
113 |
+
"total_amount": 0,
|
114 |
+
"created_at": "2025-07-06 14:53:23",
|
115 |
+
"updated_at": "2025-07-06 14:55:18"
|
116 |
+
},
|
117 |
+
{
|
118 |
+
"id": 2,
|
119 |
+
"so_number": "SO-20250708-001",
|
120 |
+
"sales_date": "2025-07-08",
|
121 |
+
"customer_id": 1,
|
122 |
+
"salesperson_id": 1,
|
123 |
+
"payment_term": "CASH",
|
124 |
+
"status": "SHIPPED",
|
125 |
+
"subtotal": 0,
|
126 |
+
"tax_amount": 0,
|
127 |
+
"discount_amount": 0,
|
128 |
+
"total_amount": 0,
|
129 |
+
"created_at": "2025-07-08 06:24:34",
|
130 |
+
"updated_at": "2025-07-10 07:08:12"
|
131 |
+
}
|
132 |
+
]
|
133 |
+
|
134 |
+
def search_orders_simulation(order_number=None, status=None):
|
135 |
+
"""模擬訂單搜尋"""
|
136 |
+
results = []
|
137 |
+
|
138 |
+
for order in orders:
|
139 |
+
match = True
|
140 |
+
|
141 |
+
# 訂單編號篩選
|
142 |
+
if order_number:
|
143 |
+
if order_number not in order["so_number"]:
|
144 |
+
match = False
|
145 |
+
|
146 |
+
# 狀態篩選
|
147 |
+
if status:
|
148 |
+
if status.upper() != order["status"]:
|
149 |
+
match = False
|
150 |
+
|
151 |
+
if match:
|
152 |
+
results.append(order)
|
153 |
+
|
154 |
+
return results
|
155 |
+
|
156 |
+
def get_status_display(status):
|
157 |
+
"""狀態中文顯示"""
|
158 |
+
status_mapping = {
|
159 |
+
'DELIVERED': '已交付',
|
160 |
+
'SHIPPED': '已出貨',
|
161 |
+
'PROCESSING': '處理中',
|
162 |
+
'CANCELLED': '已取消',
|
163 |
+
'PENDING': '待處理'
|
164 |
+
}
|
165 |
+
return status_mapping.get(status, status)
|
166 |
+
|
167 |
+
print("\n🛍️ 測試訂單查詢模擬")
|
168 |
+
print("=" * 50)
|
169 |
+
|
170 |
+
test_scenarios = [
|
171 |
+
{"query": "查詢訂單 SO-20250706-001", "order_number": "SO-20250706-001", "status": None},
|
172 |
+
{"query": "已交付的訂單", "order_number": None, "status": "DELIVERED"},
|
173 |
+
{"query": "已出貨訂單", "order_number": None, "status": "SHIPPED"},
|
174 |
+
{"query": "我的所有訂單", "order_number": None, "status": None},
|
175 |
+
{"query": "SO-20250708-001 狀態", "order_number": "SO-20250708-001", "status": None},
|
176 |
+
]
|
177 |
+
|
178 |
+
for scenario in test_scenarios:
|
179 |
+
print(f"\n查詢: '{scenario['query']}'")
|
180 |
+
results = search_orders_simulation(
|
181 |
+
order_number=scenario["order_number"],
|
182 |
+
status=scenario["status"]
|
183 |
+
)
|
184 |
+
|
185 |
+
print(f"找到 {len(results)} 個訂單:")
|
186 |
+
for order in results:
|
187 |
+
status_display = get_status_display(order["status"])
|
188 |
+
print(f" ✅ {order['so_number']} - {status_display} - {order['sales_date']} - 付款: {order['payment_term']}")
|
189 |
+
|
190 |
+
def test_natural_language_processing():
|
191 |
+
"""測試自然語言處理"""
|
192 |
+
|
193 |
+
print("\n🤖 測試自然語言訂單查詢")
|
194 |
+
print("=" * 50)
|
195 |
+
|
196 |
+
def extract_order_number_simple(message: str) -> str:
|
197 |
+
import re
|
198 |
+
so_pattern = r'SO-\d{8}-\d{3}'
|
199 |
+
match = re.search(so_pattern, message.upper())
|
200 |
+
if match:
|
201 |
+
return match.group()
|
202 |
+
|
203 |
+
date_pattern = r'\d{8}-\d{3}'
|
204 |
+
match = re.search(date_pattern, message)
|
205 |
+
if match:
|
206 |
+
return f"SO-{match.group()}"
|
207 |
+
|
208 |
+
return None
|
209 |
+
|
210 |
+
def extract_order_status_simple(message: str) -> str:
|
211 |
+
message_lower = message.lower()
|
212 |
+
status_keywords = {
|
213 |
+
'已交付': 'DELIVERED',
|
214 |
+
'已出貨': 'SHIPPED',
|
215 |
+
'出貨': 'SHIPPED',
|
216 |
+
'交付': 'DELIVERED',
|
217 |
+
'處理中': 'PROCESSING'
|
218 |
+
}
|
219 |
+
|
220 |
+
for chinese, english in status_keywords.items():
|
221 |
+
if chinese in message_lower:
|
222 |
+
return english
|
223 |
+
|
224 |
+
return None
|
225 |
+
|
226 |
+
natural_queries = [
|
227 |
+
"請問我的訂單 SO-20250706-001 現在是什麼狀態?",
|
228 |
+
"查詢已交付的所有訂單",
|
229 |
+
"20250708-001 這個訂單出貨了嗎?",
|
230 |
+
"我有哪些已出貨的訂單?",
|
231 |
+
"訂單狀態查詢"
|
232 |
+
]
|
233 |
+
|
234 |
+
for query in natural_queries:
|
235 |
+
print(f"\n自然語言查詢: '{query}'")
|
236 |
+
order_number = extract_order_number_simple(query)
|
237 |
+
status = extract_order_status_simple(query)
|
238 |
+
|
239 |
+
print(f" 提取的訂單編號: {order_number}")
|
240 |
+
print(f" 提取的狀態: {status}")
|
241 |
+
|
242 |
+
# 判斷查詢類型
|
243 |
+
if order_number and status:
|
244 |
+
query_type = "特定訂單狀態查詢"
|
245 |
+
elif order_number:
|
246 |
+
query_type = "特定訂單查詢"
|
247 |
+
elif status:
|
248 |
+
query_type = "狀態篩選查詢"
|
249 |
+
else:
|
250 |
+
query_type = "一般訂單查詢"
|
251 |
+
|
252 |
+
print(f" 查詢類型: {query_type}")
|
253 |
+
|
254 |
+
def main():
|
255 |
+
"""主函數"""
|
256 |
+
print("🚀 開始訂單查詢功能測試\n")
|
257 |
+
|
258 |
+
test_order_number_extraction()
|
259 |
+
test_order_status_extraction()
|
260 |
+
test_order_query_simulation()
|
261 |
+
test_natural_language_processing()
|
262 |
+
|
263 |
+
print("\n" + "=" * 50)
|
264 |
+
print("✅ 測試完成!")
|
265 |
+
print("\n💡 分析結果:")
|
266 |
+
print("1. 訂單編號提取邏輯能正確識別 SO- 格式")
|
267 |
+
print("2. 狀態提取支援中文和英文")
|
268 |
+
print("3. 查詢模擬能正確篩選訂單")
|
269 |
+
print("4. 自然語言處理能提取關鍵資訊")
|
270 |
+
|
271 |
+
if __name__ == "__main__":
|
272 |
+
main()
|
test_search_orders.py
ADDED
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
測試 /search 訂單查詢功能 - 基於實際銷售訂單資料
|
3 |
+
"""
|
4 |
+
|
5 |
+
import requests
|
6 |
+
import json
|
7 |
+
|
8 |
+
def test_search_api_orders():
|
9 |
+
"""測試 /search API 的訂單查詢功能"""
|
10 |
+
base_url = "http://localhost:7860"
|
11 |
+
|
12 |
+
print("🔍 測試 /search API 訂單查詢功能")
|
13 |
+
print("=" * 60)
|
14 |
+
|
15 |
+
# 測試案例基於您的實際銷售訂單資料
|
16 |
+
test_cases = [
|
17 |
+
{
|
18 |
+
"name": "查詢特定訂單編號",
|
19 |
+
"message": "查詢訂單 SO-20250706-001",
|
20 |
+
"expected": "應該找到 2025-07-06 的已交付訂單"
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"name": "查詢另一個訂單編號",
|
24 |
+
"message": "SO-20250708-001 狀態如何?",
|
25 |
+
"expected": "應該找到 2025-07-08 的已出貨訂單"
|
26 |
+
},
|
27 |
+
{
|
28 |
+
"name": "查詢已交付訂單",
|
29 |
+
"message": "查詢已交付的訂單",
|
30 |
+
"expected": "應該找到狀態為 DELIVERED 的訂單"
|
31 |
+
},
|
32 |
+
{
|
33 |
+
"name": "查詢已出貨訂單",
|
34 |
+
"message": "已出貨訂單有哪些?",
|
35 |
+
"expected": "應該找到狀態為 SHIPPED 的訂單"
|
36 |
+
},
|
37 |
+
{
|
38 |
+
"name": "一般訂單查詢",
|
39 |
+
"message": "我的訂單",
|
40 |
+
"expected": "應該顯示所有訂單"
|
41 |
+
},
|
42 |
+
{
|
43 |
+
"name": "訂單狀態查詢",
|
44 |
+
"message": "訂單狀態查詢",
|
45 |
+
"expected": "應該顯示訂單列表"
|
46 |
+
},
|
47 |
+
{
|
48 |
+
"name": "使用日期格式編號",
|
49 |
+
"message": "20250706-001 這個訂單",
|
50 |
+
"expected": "應該找到對應的 SO- 訂單"
|
51 |
+
}
|
52 |
+
]
|
53 |
+
|
54 |
+
for i, test_case in enumerate(test_cases, 1):
|
55 |
+
print(f"\n{i}. {test_case['name']}")
|
56 |
+
print(f" 查詢: '{test_case['message']}'")
|
57 |
+
print(f" 預期: {test_case['expected']}")
|
58 |
+
|
59 |
+
try:
|
60 |
+
payload = {
|
61 |
+
"message": test_case["message"],
|
62 |
+
"user_id": "test_user_search"
|
63 |
+
}
|
64 |
+
|
65 |
+
response = requests.post(
|
66 |
+
f"{base_url}/search",
|
67 |
+
json=payload,
|
68 |
+
timeout=30,
|
69 |
+
headers={"Content-Type": "application/json"}
|
70 |
+
)
|
71 |
+
|
72 |
+
print(f" 狀態碼: {response.status_code}")
|
73 |
+
|
74 |
+
if response.status_code == 200:
|
75 |
+
result = response.json()
|
76 |
+
print(f" 成功: {result.get('success', False)}")
|
77 |
+
|
78 |
+
# 顯示回應內容
|
79 |
+
response_text = result.get('text', 'No response')
|
80 |
+
if len(response_text) > 200:
|
81 |
+
print(f" 回應: {response_text[:200]}...")
|
82 |
+
else:
|
83 |
+
print(f" 回應: {response_text}")
|
84 |
+
|
85 |
+
# 檢查是否包含訂單相關資訊
|
86 |
+
if any(keyword in response_text for keyword in ['SO-', '訂單', '狀態', '已交付', '已出貨']):
|
87 |
+
print(f" ✅ 包含訂單相關資訊")
|
88 |
+
else:
|
89 |
+
print(f" ❌ 未包含預期的訂單資訊")
|
90 |
+
|
91 |
+
else:
|
92 |
+
print(f" ❌ API 錯誤: {response.text}")
|
93 |
+
|
94 |
+
except Exception as e:
|
95 |
+
print(f" ❌ 請求失敗: {str(e)}")
|
96 |
+
|
97 |
+
print("-" * 60)
|
98 |
+
|
99 |
+
def test_route_api_orders():
|
100 |
+
"""測試 /route API 對訂單查詢的路由"""
|
101 |
+
base_url = "http://localhost:7860"
|
102 |
+
|
103 |
+
print(f"\n🔄 測試 /route API 訂單查詢路由")
|
104 |
+
print("=" * 60)
|
105 |
+
|
106 |
+
# 測試訂單查詢是否被正確路由
|
107 |
+
order_queries = [
|
108 |
+
"查詢訂單 SO-20250706-001",
|
109 |
+
"我的訂單狀態",
|
110 |
+
"已交付的訂單",
|
111 |
+
"訂單編號 SO-20250708-001"
|
112 |
+
]
|
113 |
+
|
114 |
+
for i, query in enumerate(order_queries, 1):
|
115 |
+
print(f"\n{i}. 路由測試: '{query}'")
|
116 |
+
|
117 |
+
try:
|
118 |
+
payload = {
|
119 |
+
"message": query,
|
120 |
+
"user_id": "test_user_route"
|
121 |
+
}
|
122 |
+
|
123 |
+
response = requests.post(
|
124 |
+
f"{base_url}/route",
|
125 |
+
json=payload,
|
126 |
+
timeout=30,
|
127 |
+
headers={"Content-Type": "application/json"}
|
128 |
+
)
|
129 |
+
|
130 |
+
print(f" 狀態碼: {response.status_code}")
|
131 |
+
|
132 |
+
if response.status_code == 200:
|
133 |
+
result = response.json()
|
134 |
+
mode = result.get('mode', 'unknown')
|
135 |
+
success = result.get('success', False)
|
136 |
+
|
137 |
+
print(f" 路由模式: {mode}")
|
138 |
+
print(f" 成功: {success}")
|
139 |
+
|
140 |
+
# 檢查是否路由到正確的模式
|
141 |
+
if mode == "search":
|
142 |
+
print(f" ✅ 正確路由到搜尋模式")
|
143 |
+
elif mode == "product_query":
|
144 |
+
print(f" ⚠️ 路由到商品查詢模式(可能需要調整)")
|
145 |
+
else:
|
146 |
+
print(f" ❌ 路由到非預期模式: {mode}")
|
147 |
+
|
148 |
+
# 顯示部分回應
|
149 |
+
response_text = result.get('text', 'No response')
|
150 |
+
if len(response_text) > 100:
|
151 |
+
print(f" 回應: {response_text[:100]}...")
|
152 |
+
else:
|
153 |
+
print(f" 回應: {response_text}")
|
154 |
+
|
155 |
+
else:
|
156 |
+
print(f" ❌ API 錯誤: {response.text}")
|
157 |
+
|
158 |
+
except Exception as e:
|
159 |
+
print(f" ❌ 請求失敗: {str(e)}")
|
160 |
+
|
161 |
+
def test_order_query_patterns():
|
162 |
+
"""測試訂單查詢模式識別"""
|
163 |
+
|
164 |
+
print(f"\n🤖 測試訂單查詢模式識別")
|
165 |
+
print("=" * 60)
|
166 |
+
|
167 |
+
def analyze_order_query_intent(message: str):
|
168 |
+
"""簡化版訂單查詢意圖分析"""
|
169 |
+
message_lower = message.lower()
|
170 |
+
|
171 |
+
# 訂單查詢關鍵字
|
172 |
+
order_keywords = ['訂單', '訂購', '購買', 'so-', 'order']
|
173 |
+
|
174 |
+
# 狀態關鍵字
|
175 |
+
status_keywords = ['狀態', '已交付', '已出貨', '處理中', '取消', 'delivered', 'shipped']
|
176 |
+
|
177 |
+
# 查詢動作關鍵字
|
178 |
+
action_keywords = ['查詢', '搜尋', '找', '看', '檢查']
|
179 |
+
|
180 |
+
has_order_keyword = any(keyword in message_lower for keyword in order_keywords)
|
181 |
+
has_status_keyword = any(keyword in message_lower for keyword in status_keywords)
|
182 |
+
has_action_keyword = any(keyword in message_lower for keyword in action_keywords)
|
183 |
+
|
184 |
+
# 計算信心度
|
185 |
+
confidence = 0.0
|
186 |
+
if has_order_keyword:
|
187 |
+
confidence += 0.5
|
188 |
+
if has_status_keyword:
|
189 |
+
confidence += 0.3
|
190 |
+
if has_action_keyword:
|
191 |
+
confidence += 0.2
|
192 |
+
|
193 |
+
return {
|
194 |
+
"is_order_query": has_order_keyword,
|
195 |
+
"has_status": has_status_keyword,
|
196 |
+
"has_action": has_action_keyword,
|
197 |
+
"confidence": min(confidence, 1.0),
|
198 |
+
"should_route_to_search": has_order_keyword and confidence > 0.5
|
199 |
+
}
|
200 |
+
|
201 |
+
test_messages = [
|
202 |
+
"查詢訂單 SO-20250706-001",
|
203 |
+
"我的訂單狀態如何?",
|
204 |
+
"已交付的訂單有哪些?",
|
205 |
+
"SO-20250708-001 出貨了嗎?",
|
206 |
+
"訂單編號查詢",
|
207 |
+
"購買記錄",
|
208 |
+
"今天天氣如何?", # 非訂單查詢
|
209 |
+
"是否有推薦貓砂?" # 商品查詢
|
210 |
+
]
|
211 |
+
|
212 |
+
for message in test_messages:
|
213 |
+
print(f"\n訊息: '{message}'")
|
214 |
+
analysis = analyze_order_query_intent(message)
|
215 |
+
|
216 |
+
print(f" 訂單查詢: {analysis['is_order_query']}")
|
217 |
+
print(f" 包含狀態: {analysis['has_status']}")
|
218 |
+
print(f" 包含動作: {analysis['has_action']}")
|
219 |
+
print(f" 信心度: {analysis['confidence']:.2f}")
|
220 |
+
print(f" 建議路由: {'搜尋模式' if analysis['should_route_to_search'] else '其他模式'}")
|
221 |
+
|
222 |
+
def main():
|
223 |
+
"""主函數"""
|
224 |
+
print("🚀 開始 /search 訂單查詢功能測試\n")
|
225 |
+
|
226 |
+
# 檢查 API 服務是否運行
|
227 |
+
try:
|
228 |
+
response = requests.get("http://localhost:7860/health", timeout=5)
|
229 |
+
if response.status_code == 200:
|
230 |
+
print("✅ API 服務運行正常\n")
|
231 |
+
else:
|
232 |
+
print("❌ API 服務狀態異常\n")
|
233 |
+
return
|
234 |
+
except Exception as e:
|
235 |
+
print(f"❌ 無法連接到 API 服務: {str(e)}")
|
236 |
+
print("請確保 LineBot 服務正在運行在 localhost:7860\n")
|
237 |
+
# 仍然執行模式識別測試
|
238 |
+
test_order_query_patterns()
|
239 |
+
return
|
240 |
+
|
241 |
+
# 執行 API 測試
|
242 |
+
test_search_api_orders()
|
243 |
+
test_route_api_orders()
|
244 |
+
test_order_query_patterns()
|
245 |
+
|
246 |
+
print("\n" + "=" * 60)
|
247 |
+
print("✅ 測試完成!")
|
248 |
+
print("\n💡 測試總結:")
|
249 |
+
print("1. /search API 應該能正確處理訂單查詢")
|
250 |
+
print("2. 支援 SO- 格式訂單編號查詢")
|
251 |
+
print("3. 支援狀態篩選(已交付、已出貨等)")
|
252 |
+
print("4. 路由系統應該將訂單查詢導向搜尋模式")
|
253 |
+
|
254 |
+
if __name__ == "__main__":
|
255 |
+
main()
|