返回开发者新闻

通过 WhatsApp Flows 预约:创建 Node.js 后端

2024年2月27日发布者:Gafi G 与 Iryna Wagner

通过 WhatsApp Flows,您可以构建互动消息,使用户直接在 WhatsApp 上完成操作。您可借助 Flows 创建用户互动界面。例如,您可以创建简易的输入表单来收集潜客信息或提交点评。此外,您还可以设计跨多界面的复杂 Flows 用于安排预约。

本指南将指导您构建 Node.js 应用,以使用户通过 WhatsApp Flows 进行预约。您将在 WhatsApp Business 开放平台中创建 Flow,然后配置 Webhook 以接收 Flow 响应并安排预约。

前提条件

要按照本教程操作,请确保您:

创建 WhatsApp Flow

创建 WhatsApp Flow 有两种方法:使用 Flows 创建工具(可通过 WhatsApp 管理工具访问)和 Flows API。本教程介绍使用 Flows 创建工具操作。

创建 Flow

在 WhatsApp 管理工具面板的左侧菜单中,选择账户工具。然后,点击 Flows

WhatsApp 管理工具图示

点击右上角的创建 Flow

创建 flow 图示

在显示的对话框中,填写预约 Flow 的详细信息:

  • 名称 - 输入 BookAppointment,或选择您喜欢的其他名称。
  • 类别 - 选择预约
  • 模板 - 选择预约。您将使用此模板,因为该模板包含预约的必要元素。这些元素包括显示预约详细信息、用户详细信息输入、预约摘要和公司条款的界面。您可以进一步定制模板以满足使用情形。
预约图示

点击提交以创建 Flow。

您可以在创建工具用户界面右侧预览 Flow。预约界面支持用户选择预约详细信息,例如位置和日期。详细信息界面是用户输入信息的位置。摘要界面显示预约摘要。最后一个界面显示公司的条款。

您编辑 Flow 时,Flow 保持处于草稿状态。目前您可以与团队分享 Flow,但仅限于测试目的。要与大量受众分享,您需要发布 Flow。但是,发布后您将无法进行编辑。由于您仍需为此预约 Flow 添加端点网址,请暂时将其保持为草稿,然后继续下一步操作,您将在下一步配置端点。

配置 Flow 端点

WhatsApp Flows 可使您连接到外部端点。此端点可为您的 Flow 提供动态数据并控制路由。它还会接收用户通过 Flow 提交的回复。

出于测试目的,本文使用 Glitch 托管端点。使用 Glitch 是完全可选的,并非使用 Flows 的必要条件。您可以从 GitHub 复制端点代码并在您选择的任何环境中运行代码。

访问 Glitch 中的端点代码并进行重建以获得专属域名。要进行重建,点击页面顶部的重建。随即您的专属域名将作为占位符显示在 Glitch 页面右侧的输入元素中。

继续操作之前,一起先来看下代码。src 目录中包含四个 JavaScript 文件:encryption.jsflow.jskeyGenerator.jsserver.jsserver.js 是入口文件,我们先看一下此文件。

server.js

server.js 文件首先将 Express 应用程序配置为使用 express.json 中间件来解析传入的 JSON 请求。然后,它会加载端点所需的环境变量。

const { APP_SECRET, PRIVATE_KEY, PASSPHRASE, PORT = "3000" } = process.env;

APP_SECRET 用于签名验证。它可以帮助您检查消息是否通过 WhatsApp 发送,以便安全地进行处理。您将把它添加到 .env 文件。

要访问 APP_SECRET,前往 Meta 开发者中应用的面板。在左侧导航面板中的应用设置下,选择基础。点击应用密钥下的显示,然后复制密钥。然后,返回 Glitch,打开 .env 文件,并使用所复制密钥的值创建名为 APP_SECRET 的变量。

PRIVATE_KEY 可帮助解密收到的消息。PASSPHRASE 将用于验证私钥。除私钥之外,您还需要相应的公钥,稍后您将上传该公钥。切勿在此使用您生产账户的私钥。创建一个临时私钥用于在 Glitch 上进行测试,然后在您自己的基础设施中将其替换为您的生产密钥。

通过在 Glitch 终端中运行以下命令来生成公私密钥对。将 <your-passphrase> 替换为您指定的密码。点击页面底部的终端选项卡访问 Glitch 终端。

node src/keyGenerator.js <your-passphrase>

复制密码和私钥并将其粘贴至 .env 文件。点击左侧边栏中名为 .env 的文件,然后点击顶部的 ✏️ 纯文本请勿直接从用户界面编辑,这会破坏您的密钥格式

设置环境变量后,复制您生成的公钥并通过图谱 API 上传公钥

server.js 文件还包含执行不同步骤的 POST 端点:

  • 检查私钥是否存在:
       if (!PRIVATE_KEY) { throw new Error('Private key is empty.Please check your env variable "PRIVATE_KEY".'); }
  • 使用文件底部的 isRequestSignatureValid 函数验证请求签名:
if(!isRequestSignatureValid(req)) { // Return status code 432 if request signature does not match. // To learn more about return error codes visit: https://developers.facebook.com/docs/whatsapp/flows/reference/error-codes#endpoint_error_codes return res.status(432).send(); }
  • 使用 encryption.js 文件中的 decryptRequest 函数解密传入消息:
      let decryptedRequest = null; try { decryptedRequest = decryptRequest(req.body, PRIVATE_KEY, PASSPHRASE); } catch (err) { console.error(err); if (err instanceof FlowEndpointException) { return res.status(err.statusCode).send(); } return res.status(500).send(); } const { aesKeyBuffer, initialVectorBuffer, decryptedBody } = decryptedRequest; console.log("💬 Decrypted Request:", decryptedBody);
  • 确定向用户显示的 Flow 界面。稍后您将详细了解 getNextScreen 函数。

const screenResponse = await getNextScreen(decryptedBody);

       console.log("👉 Response to Encrypt:", screenResponse);
  • 加密要向用户发送的响应:
res.send(encryptResponse(screenResponse, aesKeyBuffer, initialVectorBuffer));

encryption.js

该文件包含出于安全目的对所交互消息进行加密和解密的逻辑。本教程不会重点介绍该文件的运作原理。

keyGenerator.js

如之前所述,此文件可帮助生成私钥和公钥。与 encryption.js 文件一样,本教程不会详细探讨 keyGenerator.js 文件。

flow.js

此文件中包含用于处理 Flow 的逻辑。它以名为 SCREEN_RESPONSES 的对象开头。该对象包含界面编号以及相应的详细信息,例如数据交互中使用的预设数据。该对象是在 Flow 创建工具中通过“...”> 端点 > 片段 > 响应生成的。在同一对象中,您还拥有另一编号 SUCCESS,当 Flow 成功完成时,该编号会发回客户端设备。这将关闭 Flow。

getNextScreen 函数包含指导端点向用户显示哪些 Flow 数据的逻辑。它首先从解密消息中提取必要的数据。

const { screen, data, version, action, flow_token } = decryptedBody;

WhatsApp Flows 端点通常会收到三种请求:

您可以阅读端点文档了解详细信息。

该函数使用 if 语句处理运行状况检查和错误通知并做出相应响应,如下方代码片段所示:

// handle health check request if (action === "ping") { return { version, data: { status: "active", }, }; } // handle error notification if (data?.error) { console.warn("Received client error:", data); return { version, data: { acknowledged: true, }, }; }
        

当用户点击 Flow 的行动号召 (CTA) 按钮时,将触发 INIT 操作。此操作会返回预约界面以及数据。它还会禁用位置、日期和时间下拉菜单,以确保用户填写所有栏位。

例如,仅当用户填写位置下拉菜单时才启用日期下拉菜单。当收到 data_exchange 请求时,将处理栏位的启用和禁用。

// handle initial request when opening the flow and display APPOINTMENT screen if (action === "INIT") { return { ...SCREEN_RESPONSES.APPOINTMENT, data: { ...SCREEN_RESPONSES.APPOINTMENT.data, // these fields are disabled initially.Each field is enabled when previous fields are selected is_location_enabled: false, is_date_enabled: false, is_time_enabled: false, }, }; }

对于 data_exchange 操作,使用 switch case 结构来根据界面编号确定发回哪些数据。如果界面编号为 APPOINTMENT,则仅当已选定前面的下拉菜单时才启用后续下拉菜单栏位。

// Each field is enabled only when previous fields are selected is_location_enabled: Boolean(data.department), is_date_enabled: Boolean(data.department) && Boolean(data.location), is_time_enabled: Boolean(data.department) && Boolean(data.location) && Boolean(data.date)

对于“详细信息”界面,数据对象属性的标题(例如位置和部门)是从 SCREEN_RESPONSES.APPOINTMENT.data 对象中提取的。此代码假定存在有效匹配,因此请注意,如果未找到匹配对象,可能会引发错误。

现在,我们以位置对象为例。具体位置对象的选择是通过将数组中对象的 id 属性与 data.location 的值进行匹配来确定的。

const departmentName = SCREEN_RESPONSES.APPOINTMENT.data.department.find( (dept) => dept.id === data.department ).title; const locationName = SCREEN_RESPONSES.APPOINTMENT.data.location.find( (loc) => loc.id === data.location ).title; const dateName = SCREEN_RESPONSES.APPOINTMENT.data.date.find( (date) => date.id === data.date

).title;

然后,将这些值加以组合并在响应中返回以显示“摘要”界面。

const appointment = `${departmentName} at ${locationName} ${dateName} at ${data.time}`; const details = `Name: ${data.name} Email: ${data.email} Phone: ${data.phone} "${data.more_details}"`; return { ...SCREEN_RESPONSES.SUMMARY, data: { appointment, details, // return the same fields sent from client back to submit in the next step ...data, }, };
        

从客户端提交“摘要”界面后,将向客户端设备发送成功响应以将 Flow 标记为完成。flow_token 是在将 Flow 发至用户时您可以设置的唯一标识符。

// send success response to complete and close the flow return { ...SCREEN_RESPONSES.SUCCESS, data: { extension_message_response: { params: { flow_token, }, }, }, };

“条款”界面没有要交互的数据,因此端点不会处理此界面。

将端点添加至 Flow

在 Glitch 页面的右上方,您可以通过点击更多选项菜单图标并选择复制链接来复制网址。您还可以通过点击右上方的分享来获取链接。

前往 Flow 编辑器。点击编辑器顶部显示的棕色横幅中的设置

随即将显示一个弹出窗口,允许您在 Meta 开发者中配置端点网址、业务电话号码和应用。进行必要配置后,执行运行状况检查。首先,运行互动预览并确保在互动预览设置中的获取第一个页面的数据下选择获取数据。这会向端点发送请求以检索第一个页面的数据,验证端点是否可用以及您是否已实施运行状况检查。

然后,通过点击圆点 (...) 菜单并选择发布来发布 Flow。这将使用 action === "ping" 向您的端点发送运行状况检查请求,以在发布前验证端点是否已设置妥当。

端点图示

测试 Flow

完成配置后,在 WhatsApp 创建工具用户界面中再次切换互动预览来测试 Flow。在显示的弹出窗口中,选择电话号码,然后选择获取第一个页面的数据下的获取数据选项。点击 X 图标关闭 Flow,通过行动号召 (CTA) 按钮开始重新测试 Flow。

点击日志选项卡打开 Glitch 日志。点击清除以将其清除。然后,返回到 WhatsApp 创建工具用户界面预览。点击预览 Flow。您将看到类似以下内容的信息:

预览 Flow 图示

现在,返回至 Glitch 日志。您将在解密请求下看到 INIT 操作、Flow 口令以及其他详细信息。选择部门下拉菜单后,还会向用户 Flow 返回加密响应。

解密请求图示

继续操作以选择部门。请注意 is_location_enabled 如何设置为 true 且操作已更改为 data_exchange

数据交互图示

继续测试 Flow 并观察 Glitch 日志中的数据变化。当用户通过移动设备与 Flow 互动时,也会生成类似日志。

在下一部分,您将创建 Webhook,用于在用户预约时向他们发送确认消息。

设置 Webhook

当用户完成 Flow 时,会向注册的 Webhook 发送表明 Flow 完成的消息。通过此 Webhook,您将通过聊天消息通知用户已预约成功。与端点类似,您还将使用 Glitch 进行测试。您可以在此处访问代码并进行重建

使用 Glitch 是完全可选的,并非使用 Flows 的必要条件。您可以从 GitHub 复制 Webhook 代码并在您选择的任何环境中运行代码。

设置环境变量

要设置环境变量,打开 Glitch 中的 .env 文件。将 VERIFY_TOKEN 设为所需任何字符串,将 FLOW_ID 设为您的 Flow 编号,将 GRAPH_API_TOKEN 设为您的 WhatsApp Business 商业帐号的访问口令。当您在左侧导航面板中的 WhatsApp 版块下点击 API 设置时,可以从 Meta 开发者中应用的面板获取访问口令。

API 设置图示

在显示的页面中,点击临时访问口令卡下的复制按钮。将密钥粘贴到 .env 文件中。

在 Meta 面板中订阅 Webhook

在您的 Meta 开发者账户中,点击左侧导航窗口中 WhatsApp 下的配置菜单。

配置图示

Webhook 图卡中,点击编辑。在打开的对话框中,粘贴复制的 Glitch 网址,并将 /webhook 附加到回调网址栏位中。对于验证口令栏位,添加 .env 文件的 VERIFY_TOKEN 变量中的口令。完成后,点击验证并保存。该对话框将关闭并返回主屏幕。点击管理并检查消息栏位。您的 Webhook 已经准备就绪。

Webhook 代码演示

该代码包含两个路由:POST /webhookGET /webhookGET 路由通过根据预定义的验证口令检查提供的口令是否匹配,并使用适当的状态代码和质询口令进行响应来处理 Webhook 验证请求。

const verify_token = process.env.VERIFY_TOKEN; // Parse params from the webhook verification request let mode = req.query["hub.mode"]; let token = req.query["hub.verify_token"]; let challenge = req.query["hub.challenge"]; if (mode && token) { if (mode === "subscribe" && token === verify_token) { console.log("WEBHOOK_VERIFIED"); res.status(200).send(challenge); } else { res.sendStatus(403); } }

POST /webhook 路由处理传入的 Webhook 通知。Webhook 请求可以包含不同的负载。因此,下方代码通过安全地访问请求字段来读取消息和业务电话号码,以免字段未定义。

const message = req.body.entry?.[0]?.changes[0]?.value?.messages?.[0]; const business_phone_number_id =

req.body.entry?.[0].changes?.[0].value?.metadata?.phone_number_id;

然后,它会检查传入请求是否为含“预约”一词的 "text" 类型消息。如果消息包含此词,Flow 将发至用户。Flow 消息与 flow_action: "data_exchange," 一起发送,这意味着 Flow 在启动时将向端点发出 INIT 请求以获取初始界面和数据。

if ( message.type === "text" && // for demo purposes, send the flow message whenever a user sends a message containing "appointment" message.text.body.toLowerCase().includes("appointment") ) { // send flow message as per the docs here https://developers.facebook.com/docs/whatsapp/flows/gettingstarted/sendingaflow#interactive-message-parameters await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, type: "interactive", interactive: { type: "flow", header: { type: "text", text: "Hello there 👋", }, body: { text: "Ready to transform your space?Schedule a personalized consultation with our expert team!", }, footer: { text: "Click the button below to proceed", }, action: { name: "flow", parameters: { flow_id: FLOW_ID, flow_message_version: "3", // replace flow_token with a unique identifier for this flow message to track it in your endpoint & webhook flow_token: "<FLOW_TOKEN_PLACEHOLDER>", flow_cta: "Book an appointment", flow_action: "data_exchange", }, }, }, }, }); } ...

如果传入消息类型并非 "text",代码会检查消息类型是否为 "interactive." 互动类型 "nfm_reply" 表示传入消息为 Flow 响应。然后,它会向用户发回“您已成功预约”的消息。

... if ( message.type === "interactive" && message.interactive?.type === "nfm_reply" ) { // send confirmation message await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, text: { body: "You've successfully booked an appointment" }, }, }); } ...

然后,它会将传入消息标记为已读,因此用户会看到蓝色勾号。

... // mark incoming message as read await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", status: "read", message_id: message.id, }, }); ...
        

用户体验

在此示例中,用户向您的号码发送一条包含“预约”字词的消息,然后会收到 Flow 消息。您还可以选择在进行不同互动之后或作为消息模板发送 Flow

用户将收到 Flow 消息,其中包含用于预约的行动号召 (CTA) 按钮,他们可在其中填写详细信息。接着,当他们完成 Flow 时,将收到一条确认消息。

向用户发送 Flow 图示

通过本指南,您已了解如何设置 WhatsApp Flow 以实现无缝安排预约。通过 Flow 创建工具用户界面,您创建了表单来收集用户的预约详细信息。

Flows 无需将用户重定向至外部网站进行预约,从而增强了客户体验。简易的流程使用户能够直接在 WhatsApp 内完成预约。除安排预约之外,您还可以使用 WhatsApp Flows 收集客户服务反馈,或帮助用户订阅推广信息或营销邮件。WhatsApp Flows 还可以灵活连接外部 API 或您端点中的其他应用。

使用 Flow 创建工具用户界面可以轻松创建 WhatsApp Flows。不过,您也可以使用 Flow API 以编程方式构建 Flow。如需了解更多信息,请参阅 WhatsApp Flows 文档