“在线购买,门店自提”:Bonjour Meal - 第 2 部分 - 构建购物车

1. 简介

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

上次更新时间:2020 年 10 月 30 日

在 Business Messages 中构建购物车!

这是系列 Codelab 中的第二个 Codelab,旨在介绍如何打造“在线购买、门店自提”用户体验历程。在许多电子商务历程中,购物车都是成功将用户转化为付费客户的关键。此外,通过购物车,您还可以更好地了解自己的客户,并就他们可能感兴趣的其他商品提供建议。在本 Codelab 中,我们将着重介绍如何打造购物车体验,以及如何将应用部署到 Google App Engine。

怎样才算是优质的购物车?

购物车是打造成功的在线购物体验的关键。事实证明,Business Messages 不仅善于推进与潜在客户之间的商品相关问答,而且可以推动整个购物体验,在对话中完成付款流程。

9d17537b980d0e62.png

除了优质的购物车之外,出色的购物体验还可让用户按类别浏览商品,支持商家推荐买家可能感兴趣的其他商品。向购物车添加更多商品后,用户可以查看整个购物车,并可在结账前移除或添加更多商品。

构建内容

在此 Codelab 系列的这一部分,您将为虚构公司 Bonjour Meal 扩展您在第 1 部分中构建的数字代理,以便用户浏览商品目录并将商品添加到购物车。

在本 Codelab 中,您的应用将实现下列功能:

  • 在 Business Messages 中显示问题目录
  • 推荐用户可能感兴趣的商品
  • 查看购物车中的内容并创建总价摘要

ab2fb6a4ed33a129.png

学习内容

  • 如何在 Google Cloud Platform 中的 App Engine 上部署 Web 应用
  • 如何使用永久性存储机制保存购物车的状态

本 Codelab 主要介绍如何扩展此 Codelab 系列第 1 部分的数字代理。

所需条件

  • 一个已注册并获准与 Business Messages 配合使用的 GCP 项目
  • 访问我们的开发者网站,获取相关说明
  • 一个针对您的 GCP 项目生成的服务账号 JSON 凭据文件
  • 搭载 Android 5 及更高版本的 Android 设备或 iOS 设备,须安装“Google 地图”应用
  • 具有 Web 应用编程方面的经验
  • 已连接到互联网!

2. 准备工作

此 Codelab 假定您已创建了您的第一个代理,并完成了此 Codelab 的第 1 部分。因此,我们不会介绍启用 Business Messages 和 Business Communications API、创建服务账号密钥、在 Business Communications Console 中部署应用或设置 webhook 的基础知识。话虽如此,我们将克隆一个示例应用,确保您的应用与我们所构建的应用保持一致。此外,我们将在 Google Cloud Platform 上启用 Datastore 的 API,以便保留与购物车有关的数据。

从 GitHub 克隆应用

在终端中,使用以下命令将 Django Echo Bot 代码示例克隆到项目的工作目录中:

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

将为相应服务账号创建的 JSON 凭据文件复制到示例的“resources”文件夹中,并将这些凭据重命名为“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

在此 Codelab 系列的第 1 部分,您启用了 Business Messages API、Business Communications API 和 Cloud Build API。

在本 Codelab 中,由于我们要使用 Google Datastore,因此还需要启用 Google Datastore API:

  1. 在 Google Cloud Console 中打开 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]

您刚刚部署的代码包含一个 Web 应用,该应用具有用于从 Business Messages 接收消息的 webhook。它包括我们在此 Codelab 系列的第 1 部分所做的一切。如果您尚未配置 webhook,请执行此操作。

该应用将响应一些简单的查询,例如在用户询问 Bonjour Meal 的营业时间时进行回复。您应该在移动设备上对此进行测试,方法是从 Business Communications Console 中的“Agent Information”(代理信息)页面检索测试网址。测试网址会在移动设备上启动 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 应用读取此文件!

从库存文件中读取数据

这里的库存文件是一个名为“inventory.json”的静态文件,位于 ./resources 目录中。我们需要向 view.py 添加一些 Python 逻辑,用于读取 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
...

这样,我们应该可以从库存文件中读取数据了。现在,我们需要通过一种方式将这些商品信息呈现到对话中。

呈现商品清单

为简单起见,在本 Codelab 中,我们提供了一个通用的商品清单,通过一个复合信息卡轮播界面,让所有商品目录项呈现在 Business Messages 对话中。

为查看商品清单,我们将创建一个包含文字“Show Menu”和 postbackData“show-product-catalog”的建议回复。当用户点按建议的回复且您的 Web 应用收到回传数据时,我们会发送复合信息卡轮播界面。让我们基于 views.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 会向您的 webhook 发送包含 JSON 的 postbackData,JSON 中描述了商品以及用户想要执行的操作(在购物车中添加或移除商品)。在下一部分,我们将解析像这样能够真正将商品添加到购物车的消息。

现在,我们已进行这些更改,接下来,我们要将该 Web 应用部署到 Google App Engine,并试着体验一下!

$ gcloud app deploy

在移动设备上加载对话界面后,发送消息“show-product-catalog”,您应该会看到如下所示的商品轮播界面。

4639da46bcc5230c.png

如果您点按 Add item,除了代理重复建议的回复中的回传数据之外,实际上什么都不会发生。在下一部分,我们将利用商品清单构建可添加商品的购物车。

您刚刚构建的商品清单可通过多种方式扩展。您可能有不同的饮品菜单选项或素食选项。轮播界面或建议内容信息条是让用户展开菜单选项细目,进而找到一系列所需商品的绝佳方式。作为本 Codelab 的延伸,您不妨尝试扩展商品清单系统,使用户能够在菜单中分别查看饮品和食品,甚至指定素食选项。

4. 购物车

在本 Codelab 的这一部分,我们将在上一部分(浏览可用商品)的基础上构建购物车功能。

关键的购物车体验可让用户执行诸多操作,例如将商品添加到购物车、从购物车中移除商品、跟踪购物车中每件商品的数量,以及查看购物车中的商品等。

跟踪购物车的状态意味着我们需要在 Web 应用上保留数据。为便于进行实验和部署,我们将使用 Google Cloud Platform 中的 Google Datastore 来保留数据。用户和商家之间的会话 ID 保持不变,因此我们可以使用该 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)

我们来扩展此函数,以向购物车添加商品。

将商品添加到购物车

用户点按商品轮播界面中的 Add item 建议操作后,postbackData 会包含描述用户想要执行的操作的 JSON。JSON 字典有两个键,即“action”和“item_name”,此 JSON 字典将发送到您的 webhook。“item_name”字段是与 inventory.json 中的商品关联的唯一标识符。

从该消息中解析出购物车命令和购物车商品后,我们就可以编写条件语句来添加该商品。此处要考虑的一些极端情况是: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 函数。此外,您还需要按照我们在整个 Codelab 中使用的惯例,定义用于添加商品的常量。

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 Console 的 Google Datastore 信息中心内看到这些购物车更改。请查看 Google Datastore 控制台的以下屏幕截图,其中有一个实体以会话 ID 命名,后跟与商品目录项的一些关系,以及购物车中这些商品的数量。

619dc18a8136ea69.png

在下一部分,我们将创建列出购物车中商品的方式。购物车查看机制应展示购物车中的所有商品、这些商品的数量以及从购物车中移除商品的选项。

查看购物车中的商品

列出购物车中的商品是我们了解购物车状态以及我们可以移除哪些商品的唯一途径。

我们先发送一条友好的消息,如“Here's your shopping cart:”,接着发送另一条消息,其中包含复合信息卡轮播界面,外加与“Remove one”或“Add one”相关的建议回复。此外,复合信息卡轮播界面还应列出购物车中保存的商品的数量。

在实际编写函数之前,我们需要注意一点:如果购物车里只有一种商品,我们就无法将其作为轮播界面呈现。复合信息卡轮播界面必须包含至少两张信息卡。另一方面,如果购物车中没有商品,我们会显示一条简单的消息,表明购物车是空的。

考虑到这一点,我们来定义一个名为 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)

...

请确保您已在 views.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”后,我们会获得一条表明购物车中没有任何商品的消息、一张显示购物车中的一件商品的复合信息卡,或者显示多件商品的信息卡轮播界面。此外,我们还提供如下三条建议的回复:“See total price”、“Empty the cart”和“See the menu”。

尝试部署上述代码更改,测试购物车是否在跟踪您添加的商品,以及您是否可以从对话界面查看购物车(如上面的屏幕截图所示)。您可以从添加更改的“step-2”目录中运行此命令,以部署更改。

$ gcloud app deploy

在构建从购物车移除商品的功能后,我们将在下一部分介绍如何构建查看总价的功能。get_cart_price 函数的行为类似于查看购物车的功能,因为它会交叉引用 Datastore 与 inventory.json 文件中的数据,以生成购物车总价。这对执行此 Codelab 系列下一部分(与付款处理方集成)的操作来说很方便。

从购物车中移除商品

最后,我们可以推出移除购物车中商品的功能,以此来完善购物车行为。将现有的 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 对话中查看购物车功能,请部署该应用,以便与代理互动。为此,您可以在“step-2”目录中运行此命令。

$ gcloud app deploy

5. 准备付款

为准备好在此 Codelab 系列的下一部分与付款处理方进行集成,我们需要通过一种方式来获取购物车中商品的价格。我们来构建一个函数,用于通过下列方式检索价格:交叉引用 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

准备好在此 Codelab 系列的下一部分与付款处理方集成后,我们将调用 get_cart_price 函数,将数据传递到付款处理方并启动付款流程。

同样,您可以通过部署该应用并与代理互动,在 Business Messages 对话中尝试这一购物车功能。

$ gcloud app deploy

6. 恭喜

恭喜!您已在 Business Messages 中成功构建了购物车体验。

在本 Codelab 中,我们并未介绍清空整个购物车的功能。如果您愿意,不妨尝试扩展该应用,以实现清空购物车的功能。该解决方案位于您克隆的源代码的“step-3”目录中。

在下一部分,我们将与某个外部付款处理方集成,以便用户与贵品牌完成付款交易。

怎样才算是优质的购物车?

对话中优质的购物车体验与移动应用或实体店中的别无二致。我们仅在本 Codelab 中探讨了几项功能,包括添加商品、移除商品和计算购物车价格。与现实生活中的购物车不同的是,当您在任何时候添加或移除商品时,都可以看到购物车中所有商品的价格。这些高价值功能会让您的对话式商务体验脱颖而出!

后续操作

完成上述操作后,请查看以下一些主题,了解您可以在 Business Messages 中实现哪些更复杂的互动:

参考文档