昨天我們用 Flex Message 製作了商品選單,今天要把選單和訂單邏輯串起來,讓顧客可以完成一次完整的下單流程。這包含:選商品 → 選數量 → 確認訂單 → 送出。本文將帶你設計互動邏輯,並實作對應的訊息回覆。
使用者從 Flex Message 點擊商品。
Bot 問使用者要幾份。
使用者輸入或選擇數量。
Bot 回覆訂單確認訊息(商品、數量、金額)。
使用者按下「確認」後,系統進行訂單寫入(Day 11 再串接 DB)。
這裡會遇到一個挑戰:我們必須記得使用者目前進行到哪一步。舉例來說:
A 使用者正在輸入數量。
B 使用者已經到確認階段。
C 使用者才剛點商品。
為了不搞混不同使用者的流程,我們需要「狀態管理」。
流程示意圖
每個使用者可能在不同流程階段。
必須記錄「此使用者目前在第幾步」。
可以先用簡單的 in-memory 狀態(物件暫存)實作。
之後會用 Redis 優化(Day 19)。
in-memory 狀態的意思?
in-memory 就是「存在程式記憶體裡」的資料。
當我們在 Node.js 裡宣告一個物件,例如:
const userState = {};
這個物件只會活在伺服器程式執行的記憶體裡。
所以「in-memory 狀態」就是:
用變數、物件或陣列,直接在記憶體裡暫存使用者的流程狀態。
程式還在跑 → 狀態就存在。
程式重啟、伺服器掛掉 → 狀態就消失。
Flex Message 按鈕 action 建議用 postback,避免聊天室充滿文字:
{
"type": "button",
"action": {
"type": "postback",
"label": "下單",
"data": "action=order&item=紅茶拿鐵&price=60"
}
}
什麼是 postback?
當使用者點擊按鈕時,不會在聊天室顯示文字,而是直接把一段「隱藏的資料」送到你的伺服器 Webhook。
換句話說:
message action
:使用者點按鈕後,聊天室會顯示一段文字(例如「我要紅茶拿鐵」)。
postback action
:使用者點按鈕後,聊天室 不會顯示文字,但伺服器會收到一筆事件(event),裡面包含你設定的data
。
Bot 收到 postback event:
你選擇了 紅茶拿鐵 ($60),請輸入數量
使用者輸入數字後,Bot 回覆:
你要訂購:紅茶拿鐵 x 2,共 $120
請輸入「確認」送出訂單,或輸入「取消」
使用者輸入「確認」 → Bot 回覆「訂單已送出!」(Day 11 再寫 DB)。
使用者輸入「取消」 → Bot 回覆「訂單已取消」。
const userState = {}; // 簡單的記憶體暫存
/**
* 處理 LINE Webhook 傳入的 event。
* - 支援兩種情境:
* 1) postback:使用者在 Flex Button 點「下單」後,帶 action/item/price 進來
* 2) message(text):依照使用者當前狀態(輸入數量/確認或取消)往下走
*
* 需求環境假設:
* - client:已初始化的 LINE Messaging API 客戶端
* - userState:簡單的 in-memory 狀態儲存 { [userId]: { item, price, step, quantity? } }
* 注意:未來將用 Redis/DB 持久化並做逾時機制
*/
function handleEvent(event) {
// 情境一:使用者從 Flex Message 的「下單」按鈕觸發 postback
if (event.type === "postback") {
// 解析 postback data(如 "action=order&item=紅茶拿鐵&price=60")
const data = new URLSearchParams(event.postback.data);
// 確認是下單動作
if (data.get("action") === "order") {
// 取出商品名稱與價格(⚠️實務上 price 不應信任,應以伺服端商品表/DB為準)
const item = data.get("item");
const price = data.get("price");
// 以 userId 當 key,為該使用者建立/更新訂單狀態
userState[event.source.userId] = {
item,
price: Number(price), // 確保是數字
step: "askQuantity", // 下一步:請使用者輸入數量
};
// 回覆訊息:提示輸入數量
return client.replyMessage(event.replyToken, {
type: "text",
text: `你選擇了 ${item} ($${price}),請輸入數量`,
});
}
// 情境二:一般文字訊息事件(使用者回傳數量、或輸入「確認/取消」)
} else if (event.type === "message" && event.message.type === "text") {
// 讀取該使用者的狀態(可能 undefined)
const state = userState[event.source.userId];
// 方便除錯:印出目前狀態步驟
console.log("目前該筆訂單狀態:", state?.step);
// 步驟:輸入數量
// 條件:state 存在、step 是 askQuantity,且訊息是數字
if (state?.step === "askQuantity" && !isNaN(event.message.text)) {
// 轉成整數數量
state.quantity = parseInt(event.message.text, 10);
// 切到下一步:確認
state.step = "confirm";
// 回覆:顯示品項 x 數量 與總價,並提示輸入「確認」或「取消」
return client.replyMessage(event.replyToken, {
type: "text",
text: `你要訂購:${state.item} x ${state.quantity},共 $${state.price * state.quantity}\n請輸入「確認」或「取消」`,
});
// 步驟:確認或取消
} else if (state?.step === "confirm") {
// 使用者輸入「確認」→ 送出訂單
if (event.message.text === "確認") {
// 清除此使用者的暫存狀態(避免殘留)
delete userState[event.source.userId];
// 回覆:訂單已送出
return client.replyMessage(event.replyToken, {
type: "text",
text: "訂單已送出!",
});
// 使用者輸入「取消」→ 取消訂單
} else if (event.message.text === "取消") {
// 清除此使用者的暫存狀態
delete userState[event.source.userId];
// 回覆:訂單已取消
return client.replyMessage(event.replyToken, {
type: "text",
text: "訂單已取消",
});
}
}
}
// 其他情況(沒有狀態或流程剛開始):回傳商品選單(Flex Carousel)
// 讓使用者可以直接點「下單」按鈕,觸發 postback 帶入 action/item/price
return client.replyMessage(event.replyToken, {
type: "flex",
altText: "商品選單", // 無法顯示 Flex 的裝置會看到這段文字
contents: {
type: "carousel",
contents: [
// 商品卡片 1:紅茶拿鐵
{
type: "bubble",
body: {
type: "box",
layout: "vertical",
contents: [
{ type: "text", text: "紅茶拿鐵", weight: "bold", size: "xl" },
{ type: "text", text: "$60", color: "#AAAAAA", size: "sm" },
],
},
footer: {
type: "box",
layout: "horizontal",
contents: [
{
type: "button",
action: {
type: "postback",
label: "下單",
// 這裡把訂購資訊放在 data;點下會以 postback 事件回到本函式最上方
data: "action=order&item=紅茶拿鐵&price=60",
},
},
],
},
},
// 商品卡片 2:起司蛋餅
{
type: "bubble",
body: {
type: "box",
layout: "vertical",
contents: [
{ type: "text", text: "起司蛋餅", weight: "bold", size: "xl" },
{ type: "text", text: "$45", color: "#AAAAAA", size: "sm" },
],
},
footer: {
type: "box",
layout: "horizontal",
contents: [
{
type: "button",
action: {
type: "postback",
label: "下單",
data: "action=order&item=起司蛋餅&price=45",
},
},
],
},
},
],
},
});
}
建議使用 postback,比 message
更適合流程控制。
這裡先用 in-memory 狀態,後續會改用 Redis。
還沒寫入 MongoDB,Day 11 會把訂單記錄到資料庫。
今天我們完成了:
串接商品 → 數量 → 確認 → 送出的完整流程。
建立最簡單的使用者狀態管理。
學會處理 postback 與文字輸入的結合。
重點回顧:
訂單流程需要狀態管理。
用 postback 可以避免聊天室充滿雜訊。
「確認 / 取消」是最小可行的訂單控制。
明天(Day 11),我們要把訂單流程真正「寫進 MongoDB」,完成資料落地!