線上購買店內取貨:Bonjour Meal - 第 2 部分 - 建立購物車

1. 簡介

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

上次更新時間:2020 年 10 月 30 日

在 Business Messages 上建立購物車!

本系列的第二個程式碼研究室旨在建立「線上到店購買」使用者歷程。在許多電子商務歷程中,購物車是將使用者轉換為付費客戶的關鍵。購物車也有助於進一步瞭解客戶,且能針對他們可能感興趣的其他商品提供建議。在本程式碼研究室中,我們會著重於建立購物車體驗,並將應用程式部署至 Google App Engine。

怎樣才算是良好的購物車?

購物車是確保線上購物體驗成功的關鍵。事前說,Business Messages 不僅可協助潛在客戶進行產品問答,還能促進整個購物體驗,協助你在對話中完成付款。

9d17537b980d0e62.png

除了良好的購物車外,良好的購物體驗可讓使用者依類別瀏覽商品,並方便商家推薦買家可能感興趣的其他產品。在購物車中加入更多商品後,使用者可以查看整個購物車,並且在結帳前移除商品或新增更多商品。

建構項目

在程式碼研究室系列的本節中,您將擴充自己為虛構公司 Bonjour Meal 建構的第 1 部分建立的數位服務專員,以便使用者瀏覽商品目錄,並將商品加入購物車。

在這個程式碼研究室中,您的應用程式

  • 在 Business Messages 中顯示問題目錄
  • 推薦使用者可能感興趣的商品
  • 檢查購物車內容並建立總價摘要

ab2fb6a4ed33a129.png

課程內容

  • 如何在 Google Cloud Platform 上部署 App Engine 網頁應用程式
  • 如何使用永久儲存機制儲存購物車狀態

本程式碼研究室的重點在於擴充本程式碼研究室系列第 1 單元的數位服務專員。

軟硬體需求

2. 開始設定

本程式碼研究室假設您已建立第一個代理程式,並完成程式碼研究室的第 1 部分。因此,我們不會說明在 Business Communications Console 上啟用 Business Messages 和 Business Communications API、建立服務帳戶金鑰、部署應用程式或設定 Webhook 的基本知識。因此,我們會複製範例應用程式,確保您的應用程式與建構基礎的元件一致,同時也會在 Google Cloud Platform 上為 Datastore 啟用 API,以便持續保留購物車相關資料。

從 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

在本程式碼研究室的第 1 部分中,您已啟用 Business Messages API、BusinessCommunications API 和 Cloud Build API。

在本程式碼研究室中,我們將使用 Google Datastore,因此還需要啟用這個 API:

  1. 在 Google Cloud 控制台中開啟 Google Datastore API
  2. 確認您使用正確的 GCP 專案。
  3. 點選「啟用」。

部署範例應用程式

在終端機中,前往範例的 step-2 目錄。

在終端機中執行下列指令來部署範例:

$ gcloud config set project PROJECT_ID*
$ gcloud app deploy
  • PROJECT_ID 是您透過 API 註冊的專案 ID。

在最後一個指令的輸出內容中,記下已部署應用程式的網址:

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

您剛剛部署的程式碼包含網頁應用程式,其中包含 Webhook,用來接收來自 Business Messages 的訊息。其中包含從程式碼研究室的第 1 部分完成的所有工作。如果您尚未設定 Webhook,請先完成設定。

應用程式會回應一些簡單的問題,例如使用者詢問 Bonjour Meal 的營業時間。建議您在行動裝置上透過 Business Communications 控制台的「服務專員資訊」頁面擷取的測試網址進行測試。測試網址會在行動裝置上啟動 Business Messages 體驗,方便你開始與服務專員互動。

3. 產品目錄

庫存系統

在多數情況下,您可以透過內部 API 直接整合品牌的商品目錄。在其他情況下,您可能會抓取網頁,或建立自己的庫存追蹤系統。我們的重點並非建構庫存系統;我們會使用簡單的靜態檔案來為服務專員提供圖片和產品資訊。在這個部分,我們會從這個靜態檔案擷取資訊,並在對話中顯示這項資訊,並讓使用者瀏覽可加入購物車的項目。

靜態廣告空間檔案如下所示:

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 應用程式以讀取此檔案!

從我們的目錄讀取資料

廣告空間是 ./resources 目錄中的「inventory.json」靜態檔案。我們必須新增一些 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
...

這樣我們就能取得讀取廣告空間資料所需的資料。現在我們需要在對話中顯示這項產品資訊。

顯示產品目錄

為方便本程式碼研究室實作,我們有一般的產品目錄,透過單一的複合式資訊卡輪轉介面,在 Business Messages 對話中顯示所有庫存商品。

為了查看產品目錄,我們要建立一則建議回覆,內容包括「Show Menu」文字和「show-product-catalog」。當使用者輕觸建議的回覆,而您的網頁應用程式收到回傳資料時,我們就會傳送複合式資訊卡輪轉介面。讓我們在 view.py 頂部為這個建議回覆加入新常數。

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

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

然後,我們會剖析訊息並轉送至新的函式,用於傳送含有產品目錄的複合式資訊卡輪轉介面。首先擴充 route_message 函式,以便在使用者輕觸建議回覆時呼叫函式「send_product_catalog」,接著再定義函式。

在以下程式碼片段中,在 route_message 函式的 if 陳述式中加入其他條件,檢查 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_catalogsend_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 類別的執行個體。每項建議都代表使用者在輪轉介面中挑選某項產品的情形。使用者輕觸建議回覆後,Business Messages 就會將含有 JSON 描述商品,以及使用者想採取的動作 (新增或移除購物車) 的 postbackData 傳送至你的 Webhook。在下一節中,我們將剖析類似下方的訊息,以便將商品加入購物車。

完成這些變更後,接著請將網頁應用程式部署至 Google App Engine,開始體驗服務吧!

$ gcloud app deploy

在行動裝置上載入對話介面後,請傳送「show-product-catalog」訊息,畫面上應該會顯示類似下方的產品輪轉介面。

4639da46bcc5230c.png

如果您輕觸「Add Item」,基本上不會有任何反應,但服務專員會回應建議回覆中的回傳資料。在下一節中,我們會使用產品目錄來建立要加入商品的購物車。

您剛剛建立的產品目錄可以透過多種方式擴充。您可能有不同的飲料菜單選項或素食選擇。使用輪轉介面或建議方塊是絕佳的方式,可讓使用者細查選單選項,進而找到想要的一系列產品。身為本程式碼研究室的延伸,請嘗試擴充產品目錄系統,讓使用者能在菜單中單獨查看飲品和食物,甚至指定素食選項。

4. 購物車

在程式碼研究室的這個部分,我們將從上一節建構購物車功能,藉此瀏覽可用產品。

主要的購物車體驗有很多種,使用者可以將商品加入購物車、將商品從購物車中移除、追蹤購物車中各項商品的數量,以及查看購物車中的商品。

為了持續追蹤購物車狀態,我們必須在網頁應用程式中保存資料。我們會使用 Google Cloud Platform 中的 Google Datastore 保存資料,以便簡化實驗和部署程序。使用者與商家之間的對話 ID 會保持不變,可以藉此將使用者與購物車商品建立關聯。

首先,請透過與 Google Datastore 連線,並在看見時保留對話 ID。

使用 Datastore 連線

只要購物車發生任何互動 (例如新增或刪除商品時),就會與 Google Datastore 連線。如要進一步瞭解如何使用這個用戶端程式庫與 Google Datastore 互動,請參閱官方說明文件

以下程式碼片段定義了用來更新購物車的函式。這個函式採用以下輸入:conversation_idmessagemessage 包含 JSON,說明使用者想要採取的動作,已經內建於顯示產品目錄的輪轉介面中。此函式會建立 Google Datastore 用戶端並立即擷取 ShoppingCart 實體,其中的鍵為對話 ID。

將下列函式複製到 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)

讓我們擴充這個函式,以便將商品加入購物車。

將商品加入購物車

使用者輕觸產品輪轉介面中的「新增項目」建議動作時,postbackData 會包含 JSON,用於說明使用者要採取的動作。JSON 字典有兩個鍵「action」和「item_name」,而這個 JSON 字典會傳送至您的 Webhook。「item_name」欄位是指與 Inventory.json 中商品相關聯的專屬 ID。

系統從訊息中剖析購物車指令和購物車商品後,就可以撰寫條件陳述式來新增商品。這裡要考量一些極端案例,如果 Datastore 從未看過對話 ID,或者購物車是第一次收到這個項目。以下是上述定義的 update_shopping_cart 功能擴充功能。這項變更會將 Google Datastore 保留的商品新增至購物車。

以下程式碼片段是先前新增至 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 包含 CMD_DEL_ITEM 中定義的「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,GCP 控制台的 Google Datastore 資訊主頁應該就會反映購物車的變更。請見下方的 Google Datastore 控制台螢幕截圖,系統以對話 ID 為名稱,接著以對話 ID 為名稱,接著與庫存項目和購物車內商品的數量之間的關係命名,而這個實體就是一個實體。

619dc18a8136ea69.png

在下一節中,我們會建立一個在購物車中列出商品的方法。購物車審核機制應顯示購物車中的所有商品、這些商品的數量,並提供將商品從購物車中移除的選項。

檢查購物車中的商品

我們只能瞭解購物車狀態,以及判斷應移除哪些商品,是將購物車中的商品上架,這是唯一方法。

讓我們先傳送「這是您的購物車」等易於使用的訊息,接著再傳送一則包含複合式資訊卡輪轉介面的訊息,其中含有相關的「移除一」或「新增」建議回覆。複合式資訊卡輪轉介面會額外列出購物車中儲存的商品數量。

在實際進入並編寫函式前多加留意:如果購物車中只有一種商品類型,就無法顯示為輪轉介面。複合式資訊卡輪轉介面必須包含至少兩張資訊卡。另一方面,如果購物車中沒有商品,我們會顯示簡單的訊息,指出購物車中沒有商品。

瞭解這一點之後,讓我們定義名為 send_shopping_cart 的函式。這個函式會連線至 Google Datastore,並根據對話 ID 要求 ShoppingCart 實體。完成之後,我們會呼叫 get_inventory_data 函式,並使用複合式資訊卡輪轉介面來回報購物車的狀態。我們也會依據名稱取得產品名稱的 ID,並宣告一個函式查詢 Google Datastore 來判斷該值。在產生輪轉介面時,我們可以為建議回覆建立關聯,以便刪除項目或依據產品 ID 新增項目。以下程式碼片段會執行所有這些作業。將程式碼在任何位置複製到 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)

...

請確認您已在 view.py 頂端定義 CMD_SHOW_CART,並在使用者傳送含有「show-cart」的訊息時,呼叫 send_shopping_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 函式的運作方式與「查看購物車」功能類似,運作方式在於將 Datastore 中的資料與 Inventory.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 函式,以便傳送訊息至對話 ID,指出已新增或移除商品,並提供購物車或再次查看菜單的建議。

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

這樣應該就能解決問題!提供完整的購物車體驗,使用者可以新增商品、移除商品及查看購物車中的商品。

現在,如果您想在 Business Messages 對話中查看購物車功能,請部署應用程式來與服務專員互動。方法是在步驟-2 目錄中執行下列指令。

$ gcloud app deploy

5. 準備付款

為了在本系列的下一部分與付款處理方整合,我們需要取得購物車價格的方式。讓我們建構一個函式來擷取價格,方法是交叉參照 Google Datastore 中的購物車資料,從庫存擷取每個商品的價格,然後將價格乘以購物車中每個商品的數量。

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 函式,將資料傳遞至付款處理方,並開始付款流程。

再次提醒,你可以部署應用程式並與服務專員互動,在 Business Messages 對話中試用這項購物車功能。

$ gcloud app deploy

6. 恭喜

恭喜,你已成功在 Business Messages 中建立購物車體驗。

在本程式碼研究室中,我們未探討的部分是清空購物車的功能。如有需要,您可以嘗試擴充應用程式來實現「清空購物車」功能。您可在複製原始碼的第 3 步取得解決方案。

我們會在後續部分中整合外部付款處理方,讓使用者能與品牌完成付款交易。

怎樣才算是良好的購物車?

在對話中,良好的購物車體驗與行動應用程式或實體商店並不相同。能夠新增商品、移除商品及計算購物車價格,這只是我們在本程式碼研究室中探討的幾個功能而已。不同於實體購物車的某些資訊,當您加入或移除商品時,可以隨時隨地查看購物車中所有商品的價格。這些高價值功能可讓您的對話商務體驗脫穎而出!

後續步驟

準備就緒後,請參閱以下主題,進一步瞭解 Business Messages 支援的更複雜互動:

參考說明文件