اشترِ خدمة استلام الطلب على الإنترنت من المتجر: وجبة Bonjour - الجزء 2 - إنشاء سلة التسوّق

1- مقدمة

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

تاريخ التعديل الأخير: 2020-10-30

إضافة سلة تسوّق باستخدام ميزة "الرسائل التجارية"

هذا هو الدرس التطبيقي الثاني حول الترميز في سلسلة تهدف إلى إنشاء تجربة مستخدم لخدمة "الشراء على الإنترنت من المتجر". في العديد من رحلات التجارة الإلكترونية، تكون سلة التسوّق عاملاً أساسيًا لنجاح تحويل المستخدمين إلى عملاء يدفعون رسومًا. كما أن سلة التسوق هي وسيلة لفهم عملائك بشكل أفضل وطريقة لتقديم اقتراحات بشأن سلع أخرى قد تثير اهتمامهم. في هذا الدرس التطبيقي حول الترميز، سنركّز على إنشاء تجربة سلة التسوّق ونشر التطبيق في Google App Engine.

ما الذي يجعل سلة التسوّق جيدة؟

تُعدّ سلات التسوّق عاملاً أساسيًا لنجاح تجربة التسوّق على الإنترنت. وكما اتضح، لا تقتصر ميزة "الرسائل التجارية" على تسهيل طرح الأسئلة والأجوبة على أحد المنتجات مع عميل محتمل فحسب، بل يمكنها أيضًا تسهيل تجربة التسوّق بأكملها لإكمال عملية الدفع ضمن المحادثة.

9d17537b980d0e62.png

بالإضافة إلى عربة التسوق الجيدة، تتيح تجربة التسوق الجيدة للمستخدمين تصفح العناصر حسب الفئة وتسمح للنشاط التجاري باقتراح منتجات أخرى قد يهتم بها المشتري. بعد إضافة المزيد من العناصر إلى عربة التسوق، يمكن للمستخدم مراجعة سلة التسوق بالكامل، ويكون قادرًا على إزالة العناصر أو إضافة المزيد من العناصر قبل الدفع.

ما ستنشئه

في هذا القسم من سلسلة الدرس التطبيقي حول الترميز، سيتم توسيع نطاق الوكيل الرقمي الذي أنشأته في الجزء الأول للشركة الوهمية Bonjour Meal، كي يتمكّن المستخدمون من تصفُّح كتالوج العناصر وإضافة عناصر إلى سلة التسوّق.

في هذا الدرس التطبيقي حول الترميز، سيتعرّف تطبيقك على ما يلي:

  • عرض كتالوج الأسئلة ضمن ميزة "الرسائل التجارية"
  • اقتراح عناصر قد تهمّ المستخدمين
  • مراجعة محتويات عربة التسوق وإنشاء ملخص السعر الإجمالي

ab2fb6a4ed33a129.png

المعلومات التي ستطّلع عليها

  • كيفية نشر تطبيق ويب في App Engine على Google Cloud Platform
  • كيفية استخدام آلية تخزين دائمة لحفظ حالة سلة التسوّق

يركّز هذا الدرس التطبيقي حول الترميز على توسيع نطاق الوكيل الرقمي من الجزء الأول من سلسلة الدروس التطبيقية حول الترميز.

المتطلبات

2. بدء الإعداد

يفترض هذا الدرس التطبيقي حول الترميز أنّك أنشأت وكيلك الأول وأكملت الجزء الأول من الدرس التطبيقي حول الترميز. وبالتالي، لن نتطرق إلى أساسيات تفعيل واجهات برمجة التطبيقات لميزة "الرسائل التجارية" و"اتصالات الأنشطة التجارية" أو إنشاء مفاتيح حساب الخدمة أو نشر تطبيق أو إعداد الرد التلقائي على الويب على "وحدة التحكّم في اتصالات الأنشطة التجارية". ومع ذلك، سننسخ نموذج تطبيق للتأكد من توافق تطبيقك مع ما ننشئه فوقه، وسيتم تفعيل واجهة برمجة التطبيقات لمخزن البيانات على Google Cloud Platform حتى نتمكّن من الاحتفاظ بالبيانات المتعلقة بسلة التسوق.

استنساخ التطبيق من GitHub

في وحدة طرفية، استنسِخ Django Echo Bot Sample إلى دليل عمل المشروع باستخدام الأمر التالي:

$ git clone https://github.com/google-business-communications/bm-bonjour-meal-django-starter-code

انسخ ملف بيانات اعتماد JSON الذي تم إنشاؤه لحساب الخدمة في مجلد موارد النموذج وأعِد تسمية بيانات الاعتماد إلى "bm-agent-service-account-credentials.json".

bm-bonjour-meal-django-starter-code/bonjourmeal-codelab/step-2/resources/bm-agent-service-account-credentials.json

تفعيل واجهة برمجة تطبيقات Google Datastore API

في الجزء الأول من هذا الدرس التطبيقي حول الترميز، فعَّلت واجهة برمجة تطبيقات "الرسائل التجارية" وواجهة برمجة تطبيقات "اتصالات الأنشطة التجارية" وواجهة برمجة التطبيقات Cloud Build.

بالنسبة إلى هذا الدرس التطبيقي حول الترميز، بما أنّنا سنعمل مع خدمة تخزين البيانات من Google، سنحتاج أيضًا إلى تفعيل واجهة برمجة التطبيقات هذه:

  1. افتح Google Datastore API في Google Cloud Console.
  2. تأكَّد من استخدام مشروع Google Cloud Platform الصحيح.
  3. انقر على تفعيل.

نشر نموذج التطبيق

في وحدة طرفية، انتقِل إلى دليل الخطوة 2 للنموذج.

شغِّل الأوامر التالية في وحدة طرفية لنشر النموذج:

$ gcloud config set project PROJECT_ID*
$ gcloud app deploy
  • PROJECT_ID هو معرف المشروع للمشروع الذي استخدمته للتسجيل في واجهات برمجة التطبيقات.

اطّلِع على عنوان URL للتطبيق المنشور في نتيجة الأمر الأخير:

Deployed service [default] to [https://PROJECT_ID.appspot.com]

يحتوي الرمز الذي نشرته للتو على تطبيق ويب مع ردّ تلقائي على الويب لتلقّي الرسائل من "الرسائل التجارية". ويتضمّن هذا الدرس كل الإجراءات التي أنجزناها في الجزء الأول من الدرس التطبيقي حول الترميز. يُرجى ضبط الرد التلقائي على الويب إذا لم يسبق لك إجراء ذلك.

سيجيب التطبيق على بعض الاستفسارات البسيطة، مثل سؤال المستخدم عن ساعات عمل مطعم Bonjour Meal. يجب اختبار ذلك على جهازك الجوّال من خلال عناوين URL التجريبية التي يمكنك استردادها من معلومات الوكيل ضمن وحدة التحكم في Business Communications. ستعمل عناوين URL التجريبية على إطلاق تجربة "الرسائل التجارية" على جهازك الجوّال، ويمكنك البدء في التفاعل مع وكيلك هناك.

3. كتالوج المنتجات

نظام المستودع

وفي معظم الحالات، يمكنك الدمج مباشرةً مع مستودع العلامة التجارية من خلال واجهة برمجة تطبيقات داخلية. وفي حالات أخرى، يمكنك سرقة محتوى صفحة ويب أو إنشاء نظامك الخاص لتتبع المخزون. لا ينصب تركيزنا على إنشاء نظام المخزون، حيث سنستخدم ملفًا ثابتًا بسيطًا يحتوي على صور ومعلومات عن منتج لوكيلنا. وفي هذا القسم، سنسحب المعلومات من هذا الملف الثابت، ونعرض تلك المعلومات في المحادثة، ونسمح للمستخدم بتصفّح العناصر المتاحة لإضافتها إلى سلة التسوّق.

يبدو ملف المخزون الثابت كما يلي:

bonjourmeal-codelab/step-2/resources/inventory.json

{

    "food": [
        {
            "id":0,
            "name": "Ham and cheese sandwich",
            "price": "6.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/ham-and-cheese.png",
            "remaining": 8
        },
        {
            "id":1,
            "name": "Chicken veggie wrap",
            "price": "9.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/chicken-veggie-wrap.png",
            "remaining": 2
        },
        {
            "id":2,
            "name": "Assorted cheese plate",
            "price": "7.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/assorted-cheese-plate.png",
            "remaining": 6
        },
        {
            "id":3,
            "name": "Apple walnut salad",
            "price": "12.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/apple-walnut-salad.png",
            "remaining": 1
        }
    ]
}

لننشئ تطبيق Python لقراءة هذا الملف فيه!

القراءة من مستودعنا

المستودع هو ملف ثابت باسم "inventory.json" في دليل ./resources. نحتاج إلى إضافة بعض منطق Python إلى view.py لقراءة محتوى ملف JSON ثم إظهاره في المحادثة. لننشئ دالة تقرأ البيانات من ملف JSON وتعرض قائمة المنتجات المتاحة.

ويمكن وضع تعريف الدالة هذا في أي مكان في view.py.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_inventory_data():
        f = open(INVENTORY_FILE)
        inventory = json.load(f)
        return inventory
...

من المفترض أن يمنحنا ذلك ما نحتاج إليه لقراءة البيانات من المخزون. نحتاج الآن إلى طريقة لعرض معلومات المنتج هذه في المحادثة.

عرض كتالوج المنتجات

لتبسيط هذا الدرس التطبيقي حول الترميز، لدينا كتالوج عام للمنتجات من أجل عرض جميع سلع المستودع في محادثة "الرسائل التجارية" من خلال لوحة عرض دوّارة واحدة للبطاقات التفاعلية.

لعرض كتالوج المنتجات، سننشئ ردًا مقترحًا يحتوي على النص "عرض القائمة" وبيانات الإبلاغ عن الإحالات الناجحة "show-product-catalog". وعندما ينقر المستخدمون على الرد المقترَح ويتلقى تطبيق الويب الذي تستخدمه بيانات الإبلاغ عن الإحالات الناجحة، سنرسل لوحة العرض الدوّارة للبطاقات التفاعلية. لنضيف ثابتًا جديدًا لهذا الرد المقترَح في أعلى view.py.

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_SHOW_PRODUCT_CATALOG = 'show-product-catalog'
...

من هنا، نحلّل الرسالة ونوجِّهها إلى دالة جديدة ترسل لوحة عرض دوّارة للبطاقات التفاعلية تحتوي على كتالوج المنتجات. قم أولاً بتوسيع دالة route_message لاستدعاء الدالة "send_product_catalog" عند النقر على الرد المقترَح، وبعد ذلك سنحدد الدالة.

في المقتطف التالي، أضف شرطًا إضافيًا إلى عبارة if في الدالة route_message للتحقق مما إذا كانت normalized_message تساوي الثابت الذي حددناه سابقًا، CMD_SHOW_PRODUCT_CATALOG.

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATALOG:
        send_product_catalog(conversation_id)
    else:
        echo_message(message, conversation_id)
...

لنتأكّد من إكمال العملية وتحديد send_product_catalog. تستدعي send_product_catalog الدالة get_menu_carousel,، وهي تنشئ لوحة عرض دوّارة للبطاقات التفاعلية من ملف المستودع الذي قرأناه سابقًا.

ويمكن وضع تعريفات الدوال في أي مكان في view.py. لاحظ أن المقتطف التالي يستخدم ثابتين جديدين يجب إضافتهما إلى أعلى الملف.

bonjourmeal-codelab/step-2/bopis/views.py

...

CMD_ADD_ITEM = 'add-item'
CMD_SHOW_CART = 'show-cart'

...

def get_menu_carousel():
    """Creates a sample carousel rich card.

    Returns:
       A :obj: A BusinessMessagesCarouselCard object with three cards.
    """

    inventory = get_inventory_data()

    card_content = []

    for item in inventory['food']:
        card_content.append(BusinessMessagesCardContent(
            title=item['name'],
            description=item['price'],
            suggestions=[
                BusinessMessagesSuggestion(
                    reply=BusinessMessagesSuggestedReply(
                        text='Add item',
                        postbackData='{'+f'"action":"{CMD_ADD_ITEM}","item_name":"{item["id"]}"'+'}'))

                ],
            media=BusinessMessagesMedia(
                height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                contentInfo=BusinessMessagesContentInfo(
                    fileUrl=item['image_url'],
                    forceRefresh=False))))

    return BusinessMessagesCarouselCard(
        cardContents=card_content,
        cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum.MEDIUM)

def send_product_catalog(conversation_id):
    """Sends the product catalog to the conversation_id.

    Args:
        conversation_id (str): The unique id for this user and agent.
    """
    rich_card = BusinessMessagesRichCard(carouselCard=get_menu_carousel())

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
        fallback_text += (card_content.title + '\n\n' + card_content.description
                          + '\n\n' + card_content.media.contentInfo.fileUrl
                          + '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        fallback=fallback_text,
        suggestions=[
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See my cart',
                postbackData=CMD_SHOW_CART)
            ),
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See the menu',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
        ]
        )

    send_message(message_obj, conversation_id)
...

عند فحص إنشاء عناصر لوحة العرض الدوّارة، ننشئ أيضًا مثيلاً للفئة BusinessMessagesSuggestion. يمثّل كل اقتراح اختيار المستخدِم لمنتج في لوحة العرض الدوّارة. عندما ينقر المستخدم على الردّ المقترَح، سترسِل ميزة "الرسائل التجارية" بيانات الإبلاغ عن الإحالات الناجحة التي تحتوي على ملف JSON الذي يصف العنصر والإجراء الذي يريد المستخدم اتخاذه (إضافة أو إزالة من سلة التسوّق) إلى الرد التلقائي على الويب. في القسم التالي، سنحلل الرسائل التي تبدو بهذه الطريقة لنتمكن من إضافة العنصر إلى سلة التسوق فعليًا.

والآن بعد أن أجرينا هذه التغييرات، لننشر تطبيق الويب في Google App Engine ونجرّب هذه التجربة.

$ gcloud app deploy

عند تحميل مساحة عرض المحادثة على جهازك الجوّال، أرسِل الرسالة "show-product-catalog" ويُفترض أن تظهر لك لوحة عرض دوّارة للمنتجات شبيهة بهذا الشكل.

4639da46bcc5230c.png

عند النقر على إضافة عنصر، لن يحدث شيء في الواقع باستثناء الوكيل الذي يعرض بيانات الإبلاغ عن الإحالات الناجحة من الردّ المقترَح. في القسم التالي، سنستفيد من كتالوج المنتجات ونستخدمه لإنشاء سلة التسوّق حيث ستتم إضافة السلعة.

يمكن توسيع كتالوج المنتجات الذي أنشأته للتو بعدة طرق. قد تتوفّر خيارات مختلفة لقائمة المشروبات أو خيارات نباتية. يعد استخدام العروض الدوارة أو شرائح الاقتراحات طريقة رائعة للسماح للمستخدمين بالتوغّل في خيارات القائمة للوصول إلى مجموعة من المنتجات التي يبحثون عنها. كإضافة لهذا الدرس التطبيقي حول الترميز، جرِّب تمديد نظام كتالوج المنتجات حتى يتمكّن المستخدم من عرض المشروبات بشكل منفصل عن الطعام في القائمة، أو حتى يتمكّن من تحديد الخيارات النباتية.

4. سلة التسوّق

في هذا القسم من الدرس التطبيقي حول الترميز، سننشئ وظيفة سلة التسوق بناءً على القسم السابق الذي يتيح لنا تصفح المنتجات المتاحة.

من بين أشياء كثيرة، تتيح تجربة سلة التسوق الرئيسية للمستخدمين إضافة عناصر إلى عربة التسوق، وإزالة عناصر من عربة التسوق، وتتبع عدد كل عنصر في عربة التسوق، ومراجعة العناصر في عربة التسوق.

يعني تتبع حالة عربة التسوق أننا بحاجة إلى الاحتفاظ بالبيانات على تطبيق الويب. لتبسيط إجراء التجارب والنشر، سنستخدم Google Datastore في Google Cloud Platform للاحتفاظ بالبيانات. يظل معرف المحادثة ثابتًا بين المستخدم والنشاط التجاري، لذا يمكننا استخدامه لربط المستخدمين بعناصر سلة التسوق.

لنبدأ بالاتصال بمخزن بيانات Google والاحتفاظ بمعرف المحادثة عند رؤيته.

الاتصال بمخزن البيانات

سيتم الربط بمخزن بيانات Google كلما تم تنفيذ أي تفاعل ضد سلة التسوق، على سبيل المثال، عندما يضيف أحد المستخدمين عنصرًا أو يحذفه. يمكنك معرفة المزيد من المعلومات حول استخدام مكتبة العملاء هذه للتفاعل مع Google Datastore في المستندات الرسمية.

يحدد المقتطف التالي دالة لتحديث سلة التسوق. تستخدم الدالة الإدخال التالي: conversation_id وmessage. يحتوي message على تنسيق JSON يصف الإجراء الذي يريد المستخدم اتخاذه، وهو مضمَّن مسبقًا في لوحة العرض الدوّارة التي تعرض كتالوج المنتجات. تنشئ الدالة عميل "مخزن بيانات Google" وتجلب على الفور كيان ShoppingCart، حيث يكون المفتاح هو معرِّف المحادثة.

انسخ الدالة التالية إلى ملف view.py. سنواصل التوسّع في هذا الموضوع في القسم القادم.

bonjourmeal-codelab/step-2/bopis/views.py

from google.oauth2 import service_account
from google.cloud import datastore

def update_shopping_cart(conversation_id, message):
        credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_LOCATION)

        client = datastore.Client(credentials=credentials)
        key = client.key('ShoppingCart', conversation_id)
        entity = datastore.Entity(key=key)
        result = client.get(key)
        
        # TODO: Add logic to add and remove items from cart
        
        entity.update(result)
        client.put(entity)

لنوسع هذه الدالة لإضافة عنصر إلى عربة التسوق.

إضافة سلع إلى سلة التسوّق

عندما ينقر المستخدم على الإجراء المقترَح إضافة عنصر من لوحة العرض الدوّارة للمنتج، تحتوي بيانات الإبلاغ عن الإحالات الناجحة على تنسيق JSON الذي يصف الإجراء الذي يريد المستخدم اتخاذه. يحتوي قاموس JSON على مفتاحَين، وهما "action" و "item_name" ويتم إرسال قاموس JSON هذا إلى الرد التلقائي على الويب. الحقل "item_name" هو المعرّف الفريد المرتبط بالسلعة في المستودع.json.

بمجرد أن يكون لدينا أمر سلة التسوق وعنصر سلة التسوق تم تحليلهما من الرسالة، يمكننا بعد ذلك كتابة عبارات شرطية لإضافة العنصر. بعض الحالات الهامشية التي يجب التفكير فيها هنا هي إذا لم يسبق لمخزن البيانات رؤية معرف المحادثة أو إذا كانت عربة التسوق تتلقى هذا العنصر لأول مرة. في ما يلي امتداد لوظيفة update_shopping_cart المحدّدة أعلاه. يؤدي هذا التغيير إلى إضافة عنصر إلى سلة التسوق التي تواصلها مخزن بيانات Google.

المقتطف التالي هو امتداد للدالة السابقة التي تمت إضافتها إلى view.py. يمكنك إضافة الفرق أو نسخ المقتطف واستبدال الإصدار الحالي من دالة update_shopping_cart.

bonjourmeal-codelab/step-2bopis/views.py

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']

    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        entity.update(result)
    client.put(entity)

سيتم توسيع هذه الدالة لاحقًا للتوافق مع السيناريو الذي يحتوي فيه cart_cmd على السلسلة "del-item" المحددة في CMD_DEL_ITEM.

الربط

تأكد من إضافة عناصر السباكة في دالة route_message بحيث إذا تلقيت رسالةً بإضافة عنصر إلى سلة التسوق، يُطلق على هذه الدالة update_shopping_cart. ستحتاج أيضًا إلى تحديد ثابت لإضافة عناصر باستخدام الاصطلاح الذي نستخدمه في الدرس التطبيقي حول الترميز.

bonjourmeal-codelab/step-2bopis/views.py

...

CMD_DEL_ITEM = 'del-item'

...

def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
       update_shopping_cart(conversation_id, message)
    else:
        echo_message(message, conversation_id)

...

في الوقت الحالي، لدينا إمكانية إضافة عناصر إلى عربة التسوق. إذا نشرت التغييرات في Google App Engine، من المفترض أن تتمكن من رؤية تغييرات سلة التسوّق في لوحة بيانات متجر بيانات Google الموجودة في وحدة تحكم Google Cloud Platform. انظر لقطة الشاشة أدناه من وحدة تحكم مخزن بيانات Google، هناك كيان واحد يحمل اسم معرّف المحادثة متبوعًا ببعض العلاقات مع عناصر المستودع وكمية تلك العناصر الموجودة في سلة التسوق.

619dc18a8136ea69.png

في القسم التالي، سننشئ طريقة لإدراج العناصر في عربة التسوق. يجب أن تعرض لنا آلية مراجعة سلة التسوّق كل السلع المضمّنة في سلة التسوّق وكميتها وخيارًا لإزالة سلعة منها.

مراجعة السلع في سلة التسوق

إن إدراج العناصر في سلة التسوق هو الطريقة الوحيدة التي يمكننا من خلالها فهم حالة سلة التسوق ومعرفة السلع التي يمكننا إزالتها.

لنبدأ أولاً بإرسال رسالة ودية، مثل "إليك سلة التسوق:"، متبوعة برسالة أخرى تحتوي على لوحة عرض دوّارة للبطاقات التفاعلية تتضمن الردود المقترحة ذات الصلة بـ "إزالة واحدة" أو "إضافة واحدة". ويجب أن تتضمّن لوحة العرض الدوّارة للبطاقات التفاعلية أيضًا كمية السلع المحفوظة في سلة التسوّق.

هناك شيء يجب أن يكون على دراية به قبل الدخول فعليًا إلى الدالة وكتابة الدالة: إذا كان هناك نوع واحد فقط من العناصر في سلة التسوق، فلا يمكننا عرضه كلوحة عرض دوّارة. يجب أن تحتوي لوحات العرض الدوّارة للبطاقات التفاعلية على بطاقتين على الأقل. على الجانب الآخر، إذا لم تكن هناك عناصر في سلة التسوق، فإننا نريد أن تظهر رسالة بسيطة تقول أن سلة التسوق فارغة.

مع وضع ذلك في الاعتبار، دعنا نعرّف دالة تسمى send_shopping_cart. ترتبط هذه الدالة بمخزن بيانات Google وتطلب كيان ShoppingCart استنادًا إلى معرّف المحادثة. وبعد ذلك، سنستدعي الدالة get_inventory_data ونستخدم لوحة عرض دوّارة للبطاقات التفاعلية للإبلاغ عن حالة سلة التسوّق. سنحتاج أيضًا إلى الحصول على معرّف المنتج حسب الاسم ويمكننا إعلان وظيفة للنظر في مخزن بيانات Google لتحديد هذه القيمة. وأثناء إنشاء لوحة العرض الدوّارة، يمكننا ربط الردود المُقترَحة لحذف العناصر أو إضافة عناصر حسب معرِّف المنتج. يُنفِّذ المقتطف أدناه جميع هذه العمليات. انسخ الرمز في أي مكان في view.py.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_id_by_product_name(product_name):
  inventory = get_inventory_data()
  for item in inventory['food']:
    if item['name'] == product_name:
      return int(item['id'])
  return False


def send_shopping_cart(conversation_id):
  credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)

  # Retrieve the inventory data
  inventory = get_inventory_data()

  # Pull the data from Google Datastore
  client = datastore.Client(credentials=credentials)
  key = client.key('ShoppingCart', conversation_id)
  result = client.get(key)

  shopping_cart_suggestions = [
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See total price', postbackData='show-cart-price')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='Empty the cart', postbackData='empty-cart')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See the menu', postbackData=CMD_SHOW_PRODUCT_CATALOG)),
  ]

  if result is None or len(result.items()) == 0:
    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text='There are no items in your shopping cart.',
        suggestions=shopping_cart_suggestions)

    send_message(message_obj, conversation_id)
  elif len(result.items()) == 1:

    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      fallback_text = ('You have one type of item in the shopping cart')

      rich_card = BusinessMessagesRichCard(
          standaloneCard=BusinessMessagesStandaloneCard(
              cardContent=BusinessMessagesCardContent(
                  title=product_name,
                  description=f'{quantity} in cart.',
                  suggestions=[
                      BusinessMessagesSuggestion(
                          reply=BusinessMessagesSuggestedReply(
                              text='Remove one',
                              postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
                  ],
                  media=BusinessMessagesMedia(
                      height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                      contentInfo=BusinessMessagesContentInfo(
                          fileUrl=inventory['food'][product_id]
                          ['image_url'],
                          forceRefresh=False)))))

      message_obj = BusinessMessagesMessage(
          messageId=str(uuid.uuid4().int),
          representative=BOT_REPRESENTATIVE,
          richCard=rich_card,
          suggestions=shopping_cart_suggestions,
          fallback=fallback_text)

      send_message(message_obj, conversation_id)
  else:
    cart_carousel_items = []

    # Iterate through the cart and generate a carousel of items
    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      cart_carousel_items.append(
          BusinessMessagesCardContent(
              title=product_name,
              description=f'{quantity} in cart.',
              suggestions=[
                  BusinessMessagesSuggestion(
                      reply=BusinessMessagesSuggestedReply(
                          text='Remove one',
                          postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
              ],
              media=BusinessMessagesMedia(
                  height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                  contentInfo=BusinessMessagesContentInfo(
                      fileUrl=inventory['food'][product_id]
                      ['image_url'],
                      forceRefresh=False))))

    rich_card = BusinessMessagesRichCard(
        carouselCard=BusinessMessagesCarouselCard(
            cardContents=cart_carousel_items,
            cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum
            .MEDIUM))

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
      fallback_text += (
          card_content.title + '\n\n' + card_content.description + '\n\n' +
          card_content.media.contentInfo.fileUrl +
          '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        suggestions=shopping_cart_suggestions,
        fallback=fallback_text,
    )

    send_message(message_obj, conversation_id)

...

تأكد من أنك حددت مسبقًا CMD_SHOW_CART في أعلى view.py واستدعِ send_shopping_cart إذا أرسل المستخدم رسالة تحتوي على "show-cart".

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    else:
        echo_message(message, conversation_id)
...

34801776a97056ac.png

استنادًا إلى المنطق الذي قدمناه في الدالة send_shopping_cart، عند كتابة "show-cart"، سنحصل على رسالة تفيد بأنه لا يوجد شيء في سلة التسوق أو بطاقة تفاعلية تعرض العنصر الوحيد في سلة التسوق أو لوحة عرض دوّارة لبطاقات تعرض عناصر متعددة. بالإضافة إلى ذلك، لدينا ثلاثة ردود مقترَحة: "عرض السعر الإجمالي" و"إفراغ سلة التسوق" و "عرض القائمة".

جرِّب نشر تغييرات الرمز أعلاه للتأكّد من أنّ سلة التسوّق تتتبّع السلع التي أضفتها وأنّه يمكنك مراجعة سلة التسوق من مساحة المحادثات كما هو موضّح في لقطات الشاشة أعلاه. يمكنك نشر التغييرات باستخدام هذا الأمر من دليل الخطوة 2 الذي تضيف فيه التغييرات.

$ gcloud app deploy

سنقوم بإنشاء ميزة "عرض السعر الإجمالي" في القسم التالي بعد إنشاء الوظيفة لإزالة عنصر من عربة التسوق. ستعمل الدالة get_cart_price بالطريقة نفسها التي تعمل بها ميزة "الاطّلاع على سلّة التسوّق"، ما يعني أنّها ستشير إلى البيانات في مخزن البيانات مع ملف asset.json للحصول على السعر الإجمالي لسلة التسوّق. سيكون هذا مفيدًا في الجزء التالي من الدرس التطبيقي حول الترميز حيث نتكامل مع عمليات الدفع.

إزالة السلع من سلة التسوق

أخيرًا، يمكننا إكمال سلوك سلة التسوق من خلال تقديم وظائف لإزالة سلة التسوق. استبدل دالة update_shopping_cart الحالية بالمقتطف التالي.

bonjourmeal-codelab/step-2/ bopis/views.py

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']


    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })
        elif cart_cmd == CMD_DEL_ITEM:
            # The user is trying to delete an item from an empty cart. Pass and skip
            pass

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        elif cart_cmd == CMD_DEL_ITEM:
            if result.get(item_name) is None:
                # The user is trying to remove an item that's no in the shopping cart. Pass and skip
                pass
            elif result[item_name] - 1 > 0:
                result[item_name] = result[item_name] - 1
            else:
                del result[item_name]

        entity.update(result)
    client.put(entity)

إرسال رسالة تأكيد

عندما يضيف المستخدم عنصرًا إلى سلة التسوق، يجب عليك إرسال رسالة تأكيد تقرّ بإجراءه وبأنك عالجت طلبه. لا يساعد ذلك في تحديد التوقعات فحسب، بل يساعد أيضًا في استمرار المحادثة.

لنوسِّع دالة update_shopping_cart بحيث ترسل رسالة إلى معرِّف المحادثة تفيد بأنه تمت إضافة العنصر أو إزالته، كما يقدِّم اقتراحات لمراجعة سلّة التسوّق أو عرض القائمة مرة أخرى.

bonjourmeal-codelab/step-2/bopis/views.py

def update_shopping_cart(conversation_id, message):

     # No changes to the function, except appending the following logic
     ...
   
    if cart_cmd == CMD_ADD_ITEM:
        message = 'Great! You\'ve added an item to the cart.'
    else:
        message = 'You\'ve removed an item from the cart.'

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text=message,
        suggestions=[
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='Review shopping cart',
                postbackData=CMD_SHOW_CART)
            ),
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See menu again',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
            ])
    send_message(message_obj, conversation_id)

905a1f3d89893ba0.png

يجب أن تفعل ذلك! تجربة سلة تسوّق كاملة الميزات تتيح للمستخدم إضافة سلع وإزالتها ومراجعة السلع في سلة التسوّق.

في هذه المرحلة، إذا أردت رؤية وظيفة سلة التسوّق في محادثة "الرسائل التجارية"، يمكنك نشر التطبيق للتفاعل مع الوكيل. ويمكنك إجراء ذلك من خلال تنفيذ هذا الأمر في الدليل الخاص بالخطوة 2.

$ gcloud app deploy

5. التحضير للدفعات

استعدادًا للدمج مع جهة معالجة عمليات الدفع في الجزء التالي من السلسلة، نحتاج إلى طريقة لمعرفة سعر سلة التسوّق. لننشئ دالة تسترجع السعر لنا من خلال الإشارة المتبادلة إلى بيانات سلة التسوق في مخزن بيانات Google، واسترداد سعر كل عنصر من المخزون، وضرب السعر في كمية كل عنصر في سلة التسوق.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_cart_price(conversation_id):
    # Pull the data from Google Datastore
    credentials = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_LOCATION)
    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    # Retrieve the inventory data
    inventory = get_inventory_data()
   
    # Start off with a total of 0 before adding up the total
    total_price = 0

    if len(result.items()) != 0:
      for product_name, quantity in result.items():
        total_price = total_price + float(
            inventory['food'][get_id_by_product_name(product_name)]['price']) * int(quantity)

    return total_price

...

وأخيرًا، يمكننا استهلاك هذه الدالة وإرسال رسالة إلى المستخدم.

bonjourmeal-codelab/step-2/bopis/views.py

...

def send_shopping_cart_total_price(conversation_id):
    cart_price = get_cart_price(conversation_id)

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        suggestions=[],
        text=f'Your cart\'s total price is ${cart_price}.')

    send_message(message_obj, conversation_id)
...

لربط كل شيء معًا، لنحدث الدالة route_message والثابت لتشغيل المنطق أعلاه.

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_GET_CART_PRICE = 'show-cart-price'
...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    elif normalized_message == CMD_GET_CART_PRICE:
        send_shopping_cart_total_price(conversation_id)
    else:
        echo_message(message, conversation_id)
...

في ما يلي بعض لقطات الشاشة لتوضيح ما يحققه المنطق أعلاه:

8feacf94ed0ac6c4.png

عندما نكون جاهزين للدمج مع الجهة المسؤولة عن معالجة المعاملات في الجزء التالي من الدرس التطبيقي حول الترميز، سنستدعي الدالة get_cart_price لتمرير البيانات إلى الجهة المسؤولة عن معالجة المعاملات وبدء مسار الدفع.

مرة أخرى، يمكنك تجربة وظيفة سلة التسوّق هذه في محادثة "الرسائل التجارية" من خلال تفعيل التطبيق والتفاعل مع وكيلك.

$ gcloud app deploy

6. تهانينا

تهانينا، لقد نجحت في إنشاء تجربة سلة تسوّق ضمن ميزة "الرسائل التجارية".

لم نذكر في هذا الدرس التطبيقي حول الترميز، ميزة إفراغ سلة التسوّق بالكامل. إذا كنت ترغب في ذلك، فجرب توسيع التطبيق لإكمال ميزة "إفراغ سلة التسوق". ويتوفر الحل في الخطوة 3 من رمز المصدر الذي نسخته.

في قسم مستقبلي، سندمج مع جهة خارجية مسؤولة عن معالجة المعاملات لتمكين المستخدمين من إكمال معاملة دفع مع علامتك التجارية.

ما الذي يجعل سلة التسوّق جيدة؟

لا تختلف تجربة سلة التسوق الجيدة في محادثة عن تطبيق الهاتف المحمول أو في متجر فعلي. إنّ إمكانية إضافة العناصر وإزالتها وحساب سعر سلة التسوّق ليست سوى بعض الميزات التي اكتشفناها في هذا الدرس التطبيقي حول الترميز. هناك شيء يختلف عن سلة التسوق في العالم الحقيقي وهي القدرة على معرفة سعر جميع السلع في سلة التسوق في أي لحظة، أثناء إضافة سلع أو إزالتها. هذه الأنواع من الميزات العالية القيمة ستجعل تجربة التجارة الحوارية مميّزة.

الخطوة التالية

عندما تكون مستعدًا، اطّلِع على بعض المواضيع التالية للتعرّف على التفاعلات الأكثر تعقيدًا التي يمكنك تحقيقها من خلال ميزة "الرسائل التجارية":

المستندات المرجعية